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:
First create a project on Google Cloud Console.
Next, navigate to the Credentials page (or click “Credentials” in the left sidebar).
Click “Create Credentials” and select “OAuth client ID”. Under “Application type”, select “Web application”.
Enter a name for the client, and click “Create”.
Under “Authorized JavaScript origins” add “http://localhost:5001” (this is the port we will be using)
Under “Authorized redirect URIs” add “http://localhost:5001/auth/callback”
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.....
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 theAuth
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:
User visits the site
If not authenticated, they’re redirected to
/login
User clicks the login link which sends them to Google
Google authenticates them and sends them back to your callback URL
The
get_auth
method processes the Google response and stores the token and user info in the sessionUser 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.