OAuth with Google#

FastHTML supports Oauth. Oauth is confusing. This notebook aims to be a simple quickstart for using Oauth with Google and FastHTML.

We will be working locally, which presents some minor challenges when working with Google Oauth.

Set up Oauth client#

First, we need to set up an Oauth client. To do so:

  1. First create a project on Google Cloud Console.

  2. Next, navigate to the Credentials page (or click “Credentials” in the left sidebar).

  3. Click “Create Credentials” and select “OAuth client ID”. Under “Application type”, select “Web application”.

  4. Enter a name for the client, and click “Create”.

  5. Under “Authorized JavaScript origins” add “http://localhost:5001” (this is the port we will be using)

  6. Under “Authorized redirect URIs” add “http://localhost:5001/auth/callback

  7. Create a .env file in the directory in which you will be running this test (or add it to your .env file if you already have one). Into this file, copy the following from the Google Cloud Console:

    • the “Client secret”

    • the “Client ID”

    After this, the .env file should look like this:

    GOOGLE_CLIENT_ID=XXXX.....
    GOOGLE_CLIENT_SECRET=GOCSPX-XXXX.....
    
  8. Under “OAuth consent screen”, select “External”. Under “Test users”, add your email address. Click “Save”.

A basic example FastHTML app with Oauth#

We will create a basic FastHTML app with Oauth. We will work step by step, appending each jupyter cell to the previous one in a file called app.py using %%writefile app.py.

Imports and Environment Variables#

First, we import the necessary libraries and set up the environment variables. We use the load_dotenv function to load the environment variables from the .env file.

%%writefile app.py

import os
from fasthtml.oauth import GoogleAppClient, OAuth
from fasthtml.common import *
from dotenv import load_dotenv

load_dotenv()
Overwriting app.py

Set up the Google App Client#

Next, we set up the Google App Client, which we imported from fasthtml.oauth. We use the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from the .env file to set up the client.

This creates a Google-specific client that manages the OAuth communication with Google’s servers.

%%writefile -a app.py

client = GoogleAppClient(
    os.getenv("AUTH_CLIENT_ID"),
    os.getenv("AUTH_CLIENT_SECRET"),
    redirect_uri="http://localhost:5001/auth/callback"
)
Appending to app.py

Auth Class Implementation#

Next, we will set up the Auth class that handles the Oauth flow.

This class implements a get_auth method that is called when the user successfully logs in. It stores the token and user info in the session, and redirects to the home page.

More broadly, per the docs:

Here is where you’d handle looking up or adding a user in a database, checking for some condition… or choosing the destination based on state.

Route Handling in the Auth Class#

The Auth class is a subclass of OAuth, which includes some built-in route handling. According to the docs:

It will create and handle the redirect and logout paths, and it’s up to you to handle /login (where unsuccessful login attempts will be redirected) and /error (for oauth errors).

Our implementation provides the following routes:

  • /login - Shows the initial login page with the Google OAuth link

  • / - The protected home page that shows user status. This page is protected by the Auth class, so if the user is not logged in, they will be redirected to the login page.

  • /auth/logout - Handles logging out by clearing the session

Implementing the Auth Class#

Here is the implementation of the Auth class. It overrides the get_auth method to store the token and user info in the session, and redirects to the home page after successful login.

%%writefile -a app.py

class Auth(OAuth):
    def get_auth(self, info, ident, session, state):
        # Store both the ident and the token info in session
        session['token'] = info.get('access_token')
        session['user_info'] = info
        print("OAuth info received:", info)  # Debug print
        return RedirectResponse('/', status_code=303)
Appending to app.py

You might wonder if we actually need to implement our own Auth class. We do: the OAuth class does not implement the get_auth method and will return a NotImplementedError if we try to authenticate. We need to implement get_auth in order to specify what happens when the user successfully logs in.

Set up the FastHTML app#

We will set up a simple app that just demonstrates the Oauth flow.

In part because of the routes set up by the Auth class, the flow is as follows:

  1. User visits the site

  2. If not authenticated, they’re redirected to /login

  3. User clicks the login link which sends them to Google

  4. Google authenticates them and sends them back to your callback URL

  5. The get_auth method processes the Google response and stores the token and user info in the session

  6. User is redirected to the home page with their session now containing their authentication info

%%writefile -a app.py

app = FastHTML()
oauth = Auth(app, client)

@app.get('/')
def home(auth, session): 
    # Check both auth (ident) and user_info in session
    is_authenticated = bool(auth and session.get('user_info'))
    status_color = "color: #22c55e;" if is_authenticated else "color: #ef4444;"
    status_text = "Logged In ✓" if is_authenticated else "Logged Out ✗"
    
    return Div(
        P("=== DEBUG AUTH ==="),
        P(f"Auth (ident): {auth}"),
        P(f"User info: {session.get('user_info', 'None')}"),
        P("================="),
        P(status_text, style=status_color),
        A('Login' if not is_authenticated else 'Logout', 
          href='/login' if not is_authenticated else '/auth/logout'),
    )

@app.get('/login')
def login(req): 
    return Div(
        P("Login page"), 
        A('Start OAuth', href=oauth.login_link(req))
    )

@app.get('/auth/logout')
def logout(session):
    session.clear()  # Clear all session data
    response = RedirectResponse('/', status_code=303)
    response.delete_cookie('auth')
    return response

serve(host="localhost", port=5001)
Appending to app.py

Importantly, running oauth = Auth(app, client) adds some routes to the app. According to the docs:

[The OAuth class and our custom Auth subclass] will create and handle the redirect and logout paths, and it’s up to you to handle /login (where unsuccessful login attempts will be redirected) and /error (for oauth errors).

and

When we run oauth = Auth(app, client) it adds the redirect and logout paths to the app and also adds some beforeware. This beforeware runs on any requests (apart from any specified with the skip parameter).

So the Auth class results in the following behavior:

  • if someone who isn’t logged in tries to visit the homepage, they will instead be redirected to the login page.

  • at the login page, the user can click a button to start the Oauth flow.

  • Once the Oauth flow is complete, the user will be redirected back to the /redirect route.

Now, from your terminal, run the following command to start the server:

python app.py

You should see a message in the terminal that the server is running on port 5001.

Now, navigate to http://localhost:5001/login in your browser. You should see a Google login page.