Vector 2 Vector 2
Vector 3 Vector 3
Vector 2 Vector 3
Group 6 Subtract
Rectangle 7 Rectangle 8 Rectangle 9
Rectangle 10
Vector
<- Back to Blog

Blog posts

2023-10-25

Implementing Sign In with Google

Almost any non-trivial web app needs a way to identify and authenticate users, but Reflex does not provide this functionality out of the box because there are way too many different ways to approach the problem. Thanks to the plethora of existing React components for performing auth, a wrapper can be created to include most third-party auth solutions within a Reflex app.

In this post, I'll walk through how I set up a Google API project and wrapped @react-oauth/google to provide Sign In with Google functionality in my Reflex app.

Head over to https://console.developers.google.com/apis/credentials and sign in with the Google account that should manage the app and credential tokens.

  • Click "Create Project" and give it a name. After creation the new project should be selected.
  • Click "Configure Consent Screen", Choose "External", then Create.
    • Enter App Name and User Support Email -- these will be shown to users when logging in
    • Scroll all the way down to "Developer contact information" and add your email address, click "Save and Continue"
    • Click "Add or Remove Scopes"
      • Select "Email", "Profile", and "OpenID Connect"
      • Click "Update", then "Save and Continue"
    • Add any test users that should be able to log in during development.
  • From the "Credentials" page, click "+ Create Credentials", then "OAuth client ID"
    • Select Application Type: "Web Application"
    • Add Authorized Javascript Origins: http://localhost, http://localhost:3000, https://example.com (prod domain must be HTTPS)
    • Click "Save"
  • Copy the OAuth "Client ID" and save it for later. Mine looks like 309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com

The @react-oauth/google package provides a React component that handles all of the interaction with Google's OAuth API. It has rich functionality and many options, but for the purposes of this post, we will only wrap the props needed to get basic login working.

The GoogleOAuthProvider component is responsible for downloading Google's SDK and supplying the Client ID.

Create a new file in your app directory, react_oauth_google.py.

import reflex as rx


class GoogleOAuthProvider(rx.Component):
    library = "@react-oauth/google"
    tag = "GoogleOAuthProvider"

    client_id: rx.Var[str]

The GoogleLogin component renders the familiar "Sign in with Google" button.

Since we will use the default configuration, no props are needed, however the event trigger does need to be defined so our Reflex app is able to get the token after logging in.

Define the following wrapper in the same react_oauth_google.py.

class GoogleLogin(rx.Component):
    library = "@react-oauth/google"
    tag = "GoogleLogin"

    on_success: rx.EventHandler[lambda data: [data]]

The on_success trigger is defined to pass its argument directly to the Reflex event handler.

An event handler will be used to receive the token after a successful login. Critically, the token must be verified and decoded to access the user information it contains.

The simplest way to verify the token is to use Google's own google-auth python library. Add google-auth[requests] to your app's requirements.txt and install it with pip install -r requirements.txt.

Add the necessary imports to the module where your app State is defined and set the CLIENT_ID saved earlier, as it is needed to verify the token.

from google.auth.transport import requests
from google.oauth2.id_token import verify_oauth2_token

CLIENT_ID = "309209880368-4uqd9e44h7t4alhhdqn48pvvr63cc5j5.apps.googleusercontent.com"

The on_success trigger is fired by GoogleLogin after a successful login, and it contains the id_token that provides user information. For now, this event handler will verify the token and dump its contents to the console to verify that it is working.

class State(rx.State):
    ...

    def on_success(self, id_token: dict):
        print(
            verify_oauth2_token(
                id_token["credential"],
                requests.Request(),
                CLIENT_ID,
            )
        )

With this minimal functionality in place, it should be possible to log in with one of the test accounts defined earlier on the Consent Screen configuration.

Add the GoogleOAuthProvider and GoogleLogin components linked with the previously defined CLIENT_ID and State.on_success event handler to test the functionality so far.

from .react_oauth_google import (
    GoogleOAuthProvider,
    GoogleLogin,
)


def index():
    return rx.vstack(
        GoogleOAuthProvider.create(
            GoogleLogin.create(on_success=State.on_success),
            client_id=CLIENT_ID,
        )
    )


app = rx.App()
app.add_page(index)

After a successful login, you will see the decoded JSON Web Token (JWT) with user profile information displayed in the terminal!

The GoogleLogin component does NOT store the token in any way, so it is up to our app to store and manage the credential after login. For this purpose, we will use an rx.LocalStorage Var in the State that is set in the on_success event handler.

Additionally, an rx.cached_var will be used to verify and return the decoded token info for the frontend to use.

Finally, a new logout event handler will be defined to clear out the saved token and effectively log the user out of the app.

import json

class State(rx.State):
    id_token_json: str = rx.LocalStorage()

    def on_success(self, id_token: dict):
        self.id_token_json = json.dumps(id_token)

    @rx.cached_var
    def tokeninfo(self) -> dict[str, str]:
        try:
            return verify_oauth2_token(
                json.loads(self.id_token_json)["credential"],
                requests.Request(),
                CLIENT_ID,
            )
        except Exception as exc:
            if self.id_token_json:
                print(f"Error verifying token: {exc}")
        return {}

    def logout(self):
        self.id_token_json = ""

For convenience, a token_is_valid computed var can be defined to return a simple bool if the token is valid or not. This is specifically not a cached_var because it should be re-evaluated every time it is accessed, in case the expiry time has passed.

import time


class State(rx.State):
    ...

    @rx.var
    def token_is_valid(self) -> bool:
        try:
            return bool(
                self.tokeninfo
                and int(self.tokeninfo.get("exp", 0))
                > time.time()
            )
        except Exception:
            return False

With the decoded token data provided in the state, we can render the user's name, email, avatar, and provide a logout button.

def user_info(tokeninfo: dict) -> rx.Component:
    return rx.hstack(
        rx.avatar(
            name=tokeninfo["name"],
            src=tokeninfo["picture"],
            size="lg",
        ),
        rx.vstack(
            rx.heading(tokeninfo["name"], size="md"),
            rx.text(tokeninfo["email"]),
            align_items="flex-start",
        ),
        rx.button("Logout", on_click=State.logout),
        padding="10px",
    )

Now that the user's token is stored in the state, its absence can be used to prompt for login on protected pages. For this purpose, we will define a decorator that can be applied to any page function which shows a login button if the user token is not valid.

First define the login component that will be shown to unauthenticated users.

def login() -> rx.Component:
    return rx.vstack(
        GoogleLogin.create(on_success=State.on_success),
    )

Then define the decorator that will wrap page components.

import functools


def require_google_login(page) -> rx.Component:
    @functools.wraps(page)
    def _auth_wrapper() -> rx.Component:
        return GoogleOAuthProvider.create(
            rx.cond(
                State.is_hydrated,
                rx.cond(
                    State.token_is_valid, page(), login()
                ),
                rx.spinner(),
            ),
            client_id=CLIENT_ID,
        )

    return _auth_wrapper

Content that should never be available to unauthenticated users, must be returned from a computed var that checks token validity.

class State(rx.State):
    ...

    @rx.cached_var
    def protected_content(self) -> str:
        if self.token_is_valid:
            return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo['name']}"
        return "Not logged in."

The decorator may be applied to any function that returns an rx.Component to give the user a chance to authenticate.

@rx.page(route="/protected")
@require_google_login
def protected() -> rx.Component:
    return rx.vstack(
        user_info(State.tokeninfo),
        rx.text(State.protected_content),
        rx.link("Home", href="/"),
    )

All of the code for this example is available in the "google_auth" subdirectory inside the reflex-examples repo.

import reflex as rx


class GoogleOAuthProvider(rx.Component):
    library = "@react-oauth/google"
    tag = "GoogleOAuthProvider"

    client_id: rx.Var[str]


class GoogleLogin(rx.Component):
    library = "@react-oauth/google"
    tag = "GoogleLogin"

    on_success: rx.EventHandler[lambda data: [data]]
import functools
import json
import os
import time

from google.auth.transport import requests
from google.oauth2.id_token import verify_oauth2_token

import reflex as rx

from .react_oauth_google import GoogleOAuthProvider, GoogleLogin

CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")


class State(rx.State):
    id_token_json: str = rx.LocalStorage()

    def on_success(self, id_token: dict):
        self.id_token_json = json.dumps(id_token)

    @rx.cached_var
    def tokeninfo(self) -> dict[str, str]:
        try:
            return verify_oauth2_token(
                json.loads(self.id_token_json)["credential"],
                requests.Request(),
                CLIENT_ID,
            )
        except Exception as exc:
            if self.id_token_json:
                print(f"Error verifying token: {exc}")
        return {}

    def logout(self):
        self.id_token_json = ""

    @rx.var
    def token_is_valid(self) -> bool:
        try:
            return bool(
                self.tokeninfo
                and int(self.tokeninfo.get("exp", 0)) > time.time()
            )
        except Exception:
            return False

    @rx.cached_var
    def protected_content(self) -> str:
        if self.token_is_valid:
            return f"This content can only be viewed by a logged in User. Nice to see you {self.tokeninfo['name']}"
        return "Not logged in."


def user_info(tokeninfo: dict) -> rx.Component:
    return rx.hstack(
        rx.avatar(
            name=tokeninfo["name"],
            src=tokeninfo["picture"],
            size="lg",
        ),
        rx.vstack(
            rx.heading(tokeninfo["name"], size="md"),
            rx.text(tokeninfo["email"]),
            align_items="flex-start",
        ),
        rx.button("Logout", on_click=State.logout),
        padding="10px",
    )


def login() -> rx.Component:
    return rx.vstack(
        GoogleLogin.create(on_success=State.on_success),
    )


def require_google_login(page) -> rx.Component:
    @functools.wraps(page)
    def _auth_wrapper() -> rx.Component:
        return GoogleOAuthProvider.create(
            rx.cond(
                State.is_hydrated,
                rx.cond(State.token_is_valid, page(), login()),
                rx.spinner(),
            ),
            client_id=CLIENT_ID,
        )
    return _auth_wrapper


def index():
    return rx.vstack(
        rx.heading("Google OAuth", size="lg"),
        rx.link("Protected Page", href="/protected"),
    )


@rx.page(route="/protected")
@require_google_login
def protected() -> rx.Component:
    return rx.vstack(
        user_info(State.tokeninfo),
        rx.text(State.protected_content),
        rx.link("Home", href="/"),
    )


app = rx.App()
app.add_page(index)

๐Ÿ” Happy Building ๐Ÿš€

-- Reflex Team

The Reflex logo.

Site

HomeGalleryBlogChangelog

Join Newsletter

Get the latest updates and news about Reflex.

Join Newsletter

Get the latest updates and news about Reflex.

Copyright ยฉ 2024 Pynecone, Inc.