Skip to content

Bug: KindeApiClient is not json serializable #51

@jayceslesar

Description

@jayceslesar

Prerequisites

Describe the issue

KindeApiClient is not json serializable, which makes it impossible to store one in the flask session object, and based on the examples and docs is how this entire workflow is supposed to function

Library URL

https://github.com/kinde-oss/kinde-python-sdk

Library version

1.2.7

Operating system(s)

macOS

Operating system version(s)

does not matter

Further environment details

No response

Reproducible test case URL

"""Kinde Auth Flask Extension."""

from functools import wraps
from typing import Any, Callable, Optional, TypeVar, cast

from flask import Flask, Response, redirect, request, session, url_for
from kinde_sdk import Configuration  # type: ignore
from kinde_sdk.kinde_api_client import GrantType, KindeApiClient  # type: ignore
from werkzeug.wrappers.response import Response as WerkzeugResponse

F = TypeVar("F", bound=Callable[..., Any])


class KindeFlaskExtension:
    """Flask extension for Kinde authentication."""

    def __init__(
        self,
        app: Flask,
        index_url: str,
        login_url: str,
        logout_url: str,
        kinde_host: str,
        kinde_client_id: str,
        kinde_client_secret: str,
        disable_auth: bool = False,
    ):
        """Initialize Kinde Flask Extension.

        Args:
            app: Flask application instance.
            index_url: URL to redirect after successful login.
            login_url: URL to redirect for login.
            logout_url: URL to redirect for logout.
            kinde_host: Kinde host URL.
            kinde_client_id: Kinde client ID.
            kinde_client_secret: Kinde client secret.
            disable_auth: Disable authentication for testing purposes.
        """
        self.app = app
        self.index_url = index_url
        self.login_url = login_url
        self.logout_url = logout_url
        self.kinde_host = kinde_host
        self.kinde_client_id = kinde_client_id
        self.kinde_client_secret = kinde_client_secret
        self.user_clients: dict[str, KindeApiClient] = {}
        self._register_routes(app)
        self.app.extensions = getattr(app, "extensions", {})
        self.app.extensions["kinde"] = self
        self.disable_auth = disable_auth

    def login_required(self, f: F) -> F:
        """Require authentication for a route."""
        if self.disable_auth:
            return f

        @wraps(f)
        def decorated_function(*args: Any, **kwargs: Any) -> Any:
            if kinde_client := session.get("kinde_client", None):
                if kinde_client is not None and kinde_client.is_authenticated():
                    return f(*args, **kwargs)
            return redirect(self.login_url)

        return cast(F, decorated_function)

    def _register_routes(self, app: Flask) -> None:
        @app.route("/kinde_callback")
        def callback() -> Response | WerkzeugResponse:
            redirect_url = session.pop("redirect_url", self.index_url)
            if self.disable_auth:
                return redirect(redirect_url)
            try:
                kinde_client = session["kinde_client"]
                kinde_client.fetch_token(authorization_response=str(request.url))
                user = self.get_authorized_data(kinde_client)
                user_id = user["id"]
                session["user_id"] = user_id
                return redirect(redirect_url)
            except Exception as e:
                return Response(
                    f"Error during authentication: {str(e)}",
                    status=400,
                )

        @app.route(self.login_url)
        def login() -> Response | WerkzeugResponse:
            redirect_url = session.pop("redirect_url", self.index_url)
            if self.disable_auth:
                return redirect(redirect_url)
            kinde_client = KindeApiClient(
                configuration=Configuration(host=self.kinde_host),
                domain=self.kinde_host,
                client_id=self.kinde_client_id,
                client_secret=self.kinde_client_secret,
                grant_type=GrantType.AUTHORIZATION_CODE,
                callback_url=url_for(
                    "callback",
                    _external=True,
                ),
            )
            session["kinde_client"] = kinde_client
            return redirect(kinde_client.get_login_url())

        @app.route(self.logout_url)
        def logout() -> Response | WerkzeugResponse:
            self.user_clients[session["user_id"]] = None
            session.pop("user_id", None)
            return app.redirect(self.kinde_client.logout(redirect_to=self.index_url))

    def get_authorized_data(self, kinde_client: KindeApiClient) -> dict[str, str]:
        """Get user data from Kinde API."""
        user: dict[str, str] = kinde_client.get_user_details()
        return user

Additional information

I pasted the example Im working with, but I cannot stick a client (which is specific to a user) inside of the flask session object.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions