Skip to content

v1 fixes: OAuth functionality - part I #5247

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

Merged
merged 9 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/flet/lib/src/services/shared_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ class SharedPreferencesService extends FletService {
}

Future<dynamic> _invokeMethod(String name, dynamic args) async {
debugPrint("SharedPreferencesService.$name($args)");
var prefs = await SharedPreferences.getInstance();
switch (name) {
case "set":
Expand Down
9 changes: 6 additions & 3 deletions sdk/python/packages/flet-web/src/flet_web/fastapi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
from fastapi import Request, WebSocket
from flet.controls.page import Page
from flet.controls.types import WebRenderer
from starlette.middleware.base import BaseHTTPMiddleware

from flet_web.fastapi.flet_app import (
DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
DEFAULT_FLET_SESSION_TIMEOUT,
FletApp,
app_manager,
)
from flet_web.fastapi.flet_fastapi import FastAPI
from flet_web.fastapi.flet_oauth import FletOAuth
from flet_web.fastapi.flet_static_files import FletStaticFiles
from flet_web.fastapi.flet_upload import FletUpload
from starlette.middleware.base import BaseHTTPMiddleware


def app(
Expand Down Expand Up @@ -80,8 +82,9 @@ def app(
@fastapi_app.websocket(f"/{websocket_endpoint}")
async def app_handler(websocket: WebSocket):
await FletApp(
asyncio.get_running_loop(),
session_handler,
loop=asyncio.get_running_loop(),
executor=app_manager.executor,
session_handler=session_handler,
session_timeout_seconds=session_timeout_seconds,
oauth_state_timeout_seconds=oauth_state_timeout_seconds,
upload_endpoint_path=upload_endpoint_path,
Expand Down
43 changes: 23 additions & 20 deletions sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
import os
import traceback
import weakref
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
from typing import Any, Optional

import flet_web.fastapi as flet_fastapi
import msgpack
from fastapi import WebSocket, WebSocketDisconnect
from flet_web.fastapi.flet_app_manager import app_manager

from flet.controls.base_control import BaseControl
from flet.controls.page import PageDisconnectedException
from flet.controls.update_behavior import UpdateBehavior
Expand All @@ -28,6 +27,10 @@
from flet.messaging.session import Session
from flet.utils import random_string, sha1

import flet_web.fastapi as flet_fastapi
from flet_web.fastapi.flet_app_manager import app_manager
from flet_web.fastapi.oauth_state import OAuthState

logger = logging.getLogger(flet_fastapi.__name__)

DEFAULT_FLET_SESSION_TIMEOUT = 3600
Expand All @@ -38,6 +41,7 @@ class FletApp(Connection):
def __init__(
self,
loop: asyncio.AbstractEventLoop,
executor: ThreadPoolExecutor,
session_handler,
session_timeout_seconds: int = DEFAULT_FLET_SESSION_TIMEOUT,
oauth_state_timeout_seconds: int = DEFAULT_FLET_OAUTH_STATE_TIMEOUT,
Expand All @@ -60,7 +64,8 @@ def __init__(
logger.info(f"New FletApp: {self.__id}")

self.__session = None
self.__loop = loop
self.loop = loop
self.executor = executor
self.__session_handler = session_handler
self.__session_timeout_seconds = session_timeout_seconds
self.__oauth_state_timeout_seconds = oauth_state_timeout_seconds
Expand Down Expand Up @@ -102,7 +107,7 @@ async def handle(self, websocket: WebSocket):
)

self.pubsubhub = app_manager.get_pubsubhub(
self.__session_handler, loop=self.__loop
self.__session_handler, loop=self.loop
)
self.page_url = str(websocket.url).rsplit("/", 1)[0]
self.page_name = websocket.url.path.rsplit("/", 1)[0].lstrip("/")
Expand Down Expand Up @@ -248,7 +253,9 @@ async def __on_message(self, data: Any):

elif action == ClientAction.CONTROL_EVENT:
req = ControlEventBody(**body)
await self.__session.dispatch_event(req.target, req.name, req.data)
task = asyncio.create_task(
self.__session.dispatch_event(req.target, req.name, req.data)
)

elif action == ClientAction.UPDATE_CONTROL_PROPS:
req = UpdateControlPropsBody(**body)
Expand Down Expand Up @@ -291,20 +298,16 @@ def send_message(self, message: ClientMessage):
# None,
# )

# def __process_oauth_authorize_command(self, attrs: Dict[str, Any]):
# state_id = attrs["state"]
# state = OAuthState(
# session_id=self.__get_unique_session_id(self._client_details.sessionId),
# expires_at=datetime.now(timezone.utc)
# + timedelta(seconds=self.__oauth_state_timeout_seconds),
# complete_page_html=attrs.get("completePageHtml", None),
# complete_page_url=attrs.get("completePageUrl", None),
# )
# app_manager.store_state(state_id, state)
# return (
# "",
# None,
# )
def oauth_authorize(self, attrs: dict[str, Any]):
state_id = attrs["state"]
state = OAuthState(
session_id=self.__get_unique_session_id(self.__session.id),
expires_at=datetime.now(timezone.utc)
+ timedelta(seconds=self.__oauth_state_timeout_seconds),
complete_page_html=attrs.get("completePageHtml", None),
complete_page_url=attrs.get("completePageUrl", None),
)
app_manager.store_state(state_id, state)

def __get_unique_session_id(self, session_id: str):
client_hash = sha1(f"{self.client_ip}{self.client_user_agent}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse

from flet_web.fastapi.flet_app_manager import app_manager


Expand Down Expand Up @@ -33,7 +34,7 @@ async def handle(self, request: Request):
if not session:
raise HTTPException(status_code=500, detail="Session not found")

await session._authorize_callback_async(
await session.page._authorize_callback_async(
{
"state": state_id,
"code": request.query_params.get("code"),
Expand Down
122 changes: 27 additions & 95 deletions sdk/python/packages/flet/src/flet/auth/authorization.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import asyncio
import json
import secrets
import threading
import time
from typing import List, Optional, Tuple

import httpx
from flet.auth.oauth_provider import OAuthProvider
from flet.auth.oauth_token import OAuthToken
from flet.auth.user import User
from flet.utils import is_asyncio
from flet.version import version
from oauthlib.oauth2 import WebApplicationClient
from oauthlib.oauth2.rfc6749.tokens import OAuth2Token
Expand Down Expand Up @@ -41,25 +38,13 @@ def __init__(
if s not in self.scope:
self.scope.append(s)

def dehydrate_token(self, saved_token: str):
self.__token = OAuthToken.from_json(saved_token)
self.__refresh_token()
self.__fetch_user_and_groups()

async def dehydrate_token_async(self, saved_token: str):
self.__token = OAuthToken.from_json(saved_token)
await self.__refresh_token_async()
await self.__fetch_user_and_groups_async()

# token
@property
def token(self) -> Optional[OAuthToken]:
self.__refresh_token()
return self.__token

# token_async
@property
async def token_async(self) -> Optional[OAuthToken]:
async def get_token_async(self):
await self.__refresh_token_async()
return self.__token

Expand All @@ -76,27 +61,7 @@ def get_authorization_data(self) -> Tuple[str, str]:
)
return authorization_url, self.state

def request_token(self, code: str):
req = self.__get_request_token_request(code)
with httpx.Client(follow_redirects=True) as client:
resp = client.send(req)
resp.raise_for_status()
client = WebApplicationClient(self.provider.client_id)
t = client.parse_request_body_response(resp.text)
self.__token = self.__convert_token(t)
self.__fetch_user_and_groups()

async def request_token_async(self, code: str):
req = self.__get_request_token_request(code)
async with httpx.AsyncClient(follow_redirects=True) as client:
resp = await client.send(req)
resp.raise_for_status()
client = WebApplicationClient(self.provider.client_id)
t = client.parse_request_body_response(resp.text)
self.__token = self.__convert_token(t)
await self.__fetch_user_and_groups_async()

def __get_request_token_request(self, code: str):
client = WebApplicationClient(self.provider.client_id)
data = client.prepare_request_body(
code=code,
Expand All @@ -107,24 +72,16 @@ def __get_request_token_request(self, code: str):
)
headers = self.__get_default_headers()
headers["content-type"] = "application/x-www-form-urlencoded"
return httpx.Request(
req = httpx.Request(
"POST", self.provider.token_endpoint, content=data, headers=headers
)

def __fetch_user_and_groups(self):
assert self.__token is not None
if self.fetch_user:
self.user = self.provider._fetch_user(self.__token.access_token)
if self.user is None and self.provider.user_endpoint is not None:
if self.provider.user_id_fn is None:
raise Exception(
"user_id_fn must be specified too if user_endpoint is not None"
)
self.user = self.__get_user()
if self.fetch_groups and self.user is not None:
self.user.groups = self.provider._fetch_groups(
self.__token.access_token
)
async with httpx.AsyncClient(follow_redirects=True) as client:
resp = await client.send(req)
resp.raise_for_status()
client = WebApplicationClient(self.provider.client_id)
t = client.parse_request_body_response(resp.text)
self.__token = self.__convert_token(t)
await self.__fetch_user_and_groups_async()

async def __fetch_user_and_groups_async(self):
assert self.__token is not None
Expand All @@ -151,21 +108,7 @@ def __convert_token(self, t: OAuth2Token):
refresh_token=t.get("refresh_token"),
)

def __refresh_token(self):
refresh_req = self.__get_refresh_token_request()
if refresh_req:
with httpx.Client(follow_redirects=True) as client:
refresh_resp = client.send(refresh_req)
self.__complete_refresh_token_request(refresh_resp)

async def __refresh_token_async(self):
refresh_req = self.__get_refresh_token_request()
if refresh_req:
async with httpx.AsyncClient(follow_redirects=True) as client:
refresh_resp = await client.send(refresh_req)
self.__complete_refresh_token_request(refresh_resp)

def __get_refresh_token_request(self):
if (
self.__token is None
or self.__token.expires_at is None
Expand All @@ -184,43 +127,32 @@ def __get_refresh_token_request(self):
)
headers = self.__get_default_headers()
headers["content-type"] = "application/x-www-form-urlencoded"
return httpx.Request(
refresh_req = httpx.Request(
"POST", url=self.provider.token_endpoint, content=data, headers=headers
)

def __complete_refresh_token_request(self, refresh_resp):
refresh_resp.raise_for_status()
assert self.__token is not None
client = WebApplicationClient(self.provider.client_id)
t = client.parse_request_body_response(refresh_resp.text)
if t.get("refresh_token") is None:
t["refresh_token"] = self.__token.refresh_token
self.__token = self.__convert_token(t)

def __get_user(self):
user_req = self.__get_user_request()
with httpx.Client() as client:
user_resp = client.send(user_req)
return self.__complete_user_request(user_resp)
if refresh_req:
async with httpx.AsyncClient(follow_redirects=True) as client:
refresh_resp = await client.send(refresh_req)
refresh_resp.raise_for_status()
assert self.__token is not None
client = WebApplicationClient(self.provider.client_id)
t = client.parse_request_body_response(refresh_resp.text)
if t.get("refresh_token") is None:
t["refresh_token"] = self.__token.refresh_token
self.__token = self.__convert_token(t)

async def __get_user_async(self):
user_req = self.__get_user_request()
async with httpx.AsyncClient(follow_redirects=True) as client:
user_resp = await client.send(user_req)
return self.__complete_user_request(user_resp)

def __get_user_request(self):
assert self.token is not None
assert self.provider.user_endpoint is not None
headers = self.__get_default_headers()
headers["Authorization"] = f"Bearer {self.token.access_token}"
return httpx.Request("GET", self.provider.user_endpoint, headers=headers)

def __complete_user_request(self, user_resp):
user_resp.raise_for_status()
assert self.provider.user_id_fn is not None
uj = json.loads(user_resp.text)
return User(uj, str(self.provider.user_id_fn(uj)))
user_req = httpx.Request("GET", self.provider.user_endpoint, headers=headers)
async with httpx.AsyncClient(follow_redirects=True) as client:
user_resp = await client.send(user_req)
user_resp.raise_for_status()
assert self.provider.user_id_fn is not None
uj = json.loads(user_resp.text)
return User(uj, str(self.provider.user_id_fn(uj)))

def __get_default_headers(self):
return {
Expand Down
6 changes: 0 additions & 6 deletions sdk/python/packages/flet/src/flet/auth/oauth_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,8 @@ def __init__(
def _name(self):
raise Exception("Not implemented")

def _fetch_groups(self, access_token: str) -> List[Group]:
return []

async def _fetch_groups_async(self, access_token: str) -> List[Group]:
return []

def _fetch_user(self, access_token: str) -> Optional[User]:
return None

async def _fetch_user_async(self, access_token: str) -> Optional[User]:
return None
Loading