flet-dev / flet

Flet enables developers to easily build realtime web, mobile and desktop apps in Python. No frontend experience required.
https://flet.dev
Apache License 2.0
11.22k stars 432 forks source link

OAuthProvider not working anymore since update to Flet v.0.21.1 #2853

Closed tobsome closed 7 months ago

tobsome commented 7 months ago

Description Hi guys,

I was updating my Flet from version 0.20.2 to 0.21.1 and since then my login with Okta as OAuthProvider is not working anymore. I have a normal login screen with Username/Password, login button and another button "Login with Okta". Before the update: When pressing "Login with Okta" I got redirected to the Okta login page, had to enter my credentials, got the confirmation that I logged in successfully and was redirected to my tool. All good. All apps are created correctly on Okta side.

Now after the update: I press "Login with Okta", another window opens and it's just my login screen again with Username/Password etc. input. Before the update it was a window from Okta as our IDP where I should've put my Okta credentials. That was working fine.

Code example to reproduce the issue:

### MAIN.PY

import flet
import settings
import webbrowser
from auth import LoginScreen
from flet import (
    AppBar,
    Column,
    Container,
    Row,
    Page,
    Text,
    Card,
    NavigationRail,
    NavigationRailDestination,
    icons,
    PopupMenuButton,
    PopupMenuItem,
    theme,
)

user_is_authenticated = False

class DesktopAppLayout(Row):
    """
    Setting up the UI part.
    """

    def __init__(
        self,
        title,
        page,
        pages,
        on_login_success,
        *args,
        window_size=(1200, 1000),
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.page = page
        self.pages = pages
        self.expand = True

        self.navigation_items = [navigation_item for navigation_item, _ in pages]
        self.navigation_rail = self.build_navigation_rail()
        self.update_destinations()
        self._menu_extended = True
        self.navigation_rail.extended = True
        self.on_login_success = on_login_success

        self.menu_panel = Row(
            controls=[self.navigation_rail],
            spacing=0,
            tight=True,
        )

        page_contents = [page_content for _, page_content in pages]
        self.content_area = Column(page_contents, expand=True)

        self._was_portrait = self.is_portrait()
        self._panel_visible = self.is_landscape()
        self.set_content()
        self._change_displayed_page()
        self.page.on_resize = self.handle_resize

        self.page.appbar = self.create_appbar()

        self.window_size = window_size
        self.page.window_width, self.page.window_height = self.window_size
        self.page.title = title

    def select_page(self, page_number):
        self.navigation_rail.selected_index = page_number
        self._change_displayed_page()

    def _navigation_change(self, e):
        self._change_displayed_page()
        self.page.update()

    def _change_displayed_page(self):
        page_number = self.navigation_rail.selected_index
        for i, content_page in enumerate(self.content_area.controls):
            content_page.visible = page_number == i

    def build_navigation_rail(self):
        return NavigationRail(
            selected_index=0,
            label_type="none",
            on_change=self._navigation_change,
        )

    def update_destinations(self):
        self.navigation_rail.destinations = self.navigation_items
        self.navigation_rail.label_type = "all"

    def handle_resize(self, e):
        pass

    def set_content(self):
        self.controls = [self.menu_panel, self.content_area]
        self.update_destinations()
        self.navigation_rail.extended = self._menu_extended
        self.menu_panel.visible = self._panel_visible

    def is_portrait(self) -> bool:
        return self.page.height >= self.page.width

    def is_landscape(self) -> bool:
        return self.page.width > self.page.height

    def logout_user(self):
        global user_is_authenticated, logged_in_user_email
        # Reset session and user data
        user_is_authenticated = False
        logged_in_user_email = None
        self.page.session.clear()
        self.page.client_storage.clear()

        # Clear the page, remove the AppBar, and load the login screen again
        self.page.controls.clear()
        self.page.appbar = None  # This hides the AppBar
        login_screen = LoginScreen(self.page, self.on_login_success)
        self.page.add(login_screen)
        if self.page is not None:
            self.page.update()
        else:
            print("Warning: self.page is None in logout_user method")

    def create_appbar(self) -> AppBar:
        global user_is_authenticated, logged_in_user_email, user

        version_text = Text(f"{settings.version_number}", size=10, color="#FFFFFF")

        def logout_clicked(e):
            self.logout_user()

        help_menu = PopupMenuButton(
            icon=icons.HELP,
            items=[
                PopupMenuItem(
                    icon=icons.CONTACT_SUPPORT,
                    text="Documentation",
                    on_click=lambda e: webbrowser.open_new_tab(
                        "https://google.de"
                    ),
                ),
                PopupMenuItem(
                    icon=icons.BUG_REPORT,
                    text="Report a bug",
                    on_click=lambda e: webbrowser.open_new_tab(
                        "https://google.de"
                    ),
                ),
            ],
        )

        user_menu = PopupMenuButton(
            icon=icons.PERSON,
            items=[
                PopupMenuItem(
                    text=f"User: {logged_in_user_email}"
                    if logged_in_user_email
                    else "Not logged in"
                ),
                PopupMenuItem(icon=icons.SETTINGS, text="Settings"),
                PopupMenuItem(
                    icon=icons.LOGOUT, text="Logout", on_click=logout_clicked
                ),
            ],
        )

        appbar = AppBar(
            title=Text("Test Tool"),
            center_title=True,
            elevation=8,
            actions=[version_text, help_menu, user_menu],
        )

        return appbar

def create_page(title: str, body: str):
    return Row(
        controls=[
            Column(
                horizontal_alignment="stretch",
                controls=[
                    Card(
                        content=Container(Text(title, weight="bold"), padding=8),
                    ),
                    Text(body),
                ],
            ),
        ],
    )

def main(page: Page):
    page.theme = theme.Theme(color_scheme_seed="teal")
    page.theme_mode = settings.theme_mode

    def on_login_success(state, session, client_storage):
        global logged_in_user_email

        if session:
            logged_in_user_email = page.auth.user["email"]
            page.controls.clear()  # Clear all controls and sessions from the page
            load_main_app_layout()

    def load_main_app_layout():
        pages = [
            (
                NavigationRailDestination(
                    icon=icons.INFO,
                    selected_icon=icons.INFO,
                    label="About this tool",
                ),
                create_page(
                    "About this tool",
                    "Nunc elementum lol elit id arcu sagittis, id molestie libero efficitur. Aliquam eget erat posuere, tempor enim sed, varius nibh. Suspendisse potenti. Sed interdum nunc rhoncus justo gravida, ac sodales mi malesuada.",
                ),
            ),
            (
                NavigationRailDestination(
                    icon=icons.EDIT_DOCUMENT,
                    selected_icon=icons.EDIT_DOCUMENT,
                    label="Welcome Sheet Creator",
                ),
                WelcomeSheetCreator(),
            ),
        ]

        menu_layout = DesktopAppLayout(
            page=page,
            pages=pages,
            title="Test Tool",
            on_login_success=on_login_success,
        )

        page.add(menu_layout)

    # Check if user is authenticated, if not login
    if not user_is_authenticated:
        login_screen = LoginScreen(page, on_login_success)
        page.add(login_screen)
    else:
        load_main_app_layout()

if __name__ == "__main__":
    flet.app(main, assets_dir="assets", port=8550, view=flet.AppView.WEB_BROWSER)
### AUTH.PY

import credentials.credentials
import flet
import logging
import settings
from flet import (
    ElevatedButton,
    LoginEvent,
    UserControl,
    TextField,
    Column,
    alignment,
    Container,
)

class AuthenticationLogFilter(logging.Filter):
    """
    Class for group creation log filtering.
    """

    def filter(self, record):
        return "[Authentication]" in record.getMessage()

logging.basicConfig(
    filename=settings.general_logs,
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Authentication specific logging
authentication_logger = logging.getLogger()
authentication_handler = logging.FileHandler(settings.authentication_logs)
authentication_handler.setLevel(logging.INFO)
authentication_handler.setFormatter(
    logging.Formatter(
        "%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
    )
)
authentication_handler.addFilter(AuthenticationLogFilter())
authentication_logger.addHandler(authentication_handler)

class LoginScreen(UserControl):
    """
    When user is not logged in, log in.
    """

    def __init__(self, page, on_login_success, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.page = page
        self.on_login_success = on_login_success
        self.okta_provider = flet.auth.OAuthProvider(
            authorization_endpoint=settings.OKTA_AUTHORIZATION_ENDPOINT,
            token_endpoint=settings.OKTA_OAUTH_TOKEN_ENDPOINT,
            user_endpoint=settings.OKTA_OAUTH_USER_ENDPOINT,
            user_scopes=settings.OKTA_OAUTH_SCOPES,
            user_id_fn=lambda u: u["sub"],
            client_id=credentials.credentials.OKTA_OAUTH_CLIENT_ID,
            client_secret=credentials.credentials.OKTA_OAUTH_CLIENT_SECRET,
            redirect_url=settings.OKTA_OAUTH_REDIRECT_URL,
        )

        self.username = TextField(label="Username/Email", width=300)
        self.password = TextField(label="Password", password=True, width=300)

    def build(self):
        login_button = ElevatedButton("Login", width=300)
        okta_login_button = ElevatedButton(
            "Login with Okta", on_click=self.login_with_okta, width=300
        )

        self.login_button = login_button
        self.okta_login_button = okta_login_button

        self.page.on_login = self.on_login

        login_form = Column(
            controls=[
                self.username,
                self.password,
                login_button,
                okta_login_button,
            ],
            alignment=alignment.center,
            expand=True,
        )

        # Use a Container to center the login form both vertically and horizontally
        center_container = Container(content=login_form, alignment=alignment.center)

        # Ensure the page content is centered
        return Container(
            content=center_container, expand=True, alignment=alignment.center
        )

    def login_with_okta(self, e):
        self.page.login(self.okta_provider)

    def on_login(self, e: LoginEvent):
        if e.error:
            logging.error(f"[Authentication]: {e.error}")
        else:
            # Safely access auth and user attributes
            auth = getattr(self.page, "auth", None)
            user = getattr(auth, "user", None) if auth else None
            user_id = getattr(user, "id", None) if user else None

            if user_id:
                logging.info(
                    f"[Authentication]: Login successfully with user id '{user_id}'"
                )
                self.toggle_login_buttons(True)
                # Ensure to check for the presence of token and access_token before using them
                if auth.token and getattr(auth.token, "access_token", None):
                    self.page.session.set(user_id, auth.token.access_token)
                    self.page.client_storage.set(user_id, self.page.session_id)
                    # Call on_login_success if it exists and pass parameters
                    if hasattr(self, "on_login_success") and callable(
                        self.on_login_success
                    ):
                        self.on_login_success(
                            self.page.auth.state,
                            self.page.session,
                            self.page.client_storage,
                        )
            else:
                logging.error(
                    "[Authentication]: Login failed, user information is not available."
                )

    def toggle_login_buttons(self, logged_in):
        self.login_button.visible = not logged_in
        self.okta_login_button.visible = not logged_in
        self.page.update()

Describe the results you received: The new window which opens a new window should be for my login credentials is just opening the login screen again now.

Describe the results you expected: Same result like before: `You are logged in successfully. You can close the window now!´

Additional information you deem important (e.g. issue happens only occasionally): Every time since the update.

Flet version (pip show flet):

Name: flet
Version: 0.21.1
Summary: Flet for Python - easily build interactive multi-platform apps in Python
Home-page: 
Author: Appveyor Systems Inc.
Author-email: hello@flet.dev
License: Apache-2.0
Location: /opt/homebrew/lib/python3.11/site-packages
Requires: cookiecutter, fastapi, flet-runtime, packaging, qrcode, uvicorn, watchdog
Required-by: 

Give your requirements.txt file (don't pip freeze, instead give direct packages):

(The requirements)

Operating system: macOS

Additional environment details:

FeodorFitsner commented 7 months ago

In Flet 0.21.0, because of migration to FastAPI web server, OAuth handler URL endpoint changed from /api/oauth/redirect to /oauth_callback. Use FLET_OAUTH_CALLBACK_HANDLER_ENDPOINT env var to customize.

FeodorFitsner commented 7 months ago

...or update callback URL in OAuth app registration and provider constructor.

tobsome commented 7 months ago

Perfect! It works again! Thanks a lot, @FeodorFitsner! 😍

Roh23 commented 5 months ago

please update the documentation. This took me two days to resolve.

FeodorFitsner commented 5 months ago

@Roh23 sorry about that! What guide did you use?