FastAPI with Google OAuth - Part 1
Implementing Google OAuth as your authentication layer in FastAPI.
So you want to create a FastAPI application and protect it from unauthorized users. Thanks to Google OAuth and a few Python libraries, it's fairly straightforward to do so!
The code for the tutorial can be found in this GitHub repo. I recommend cloning it as you follow along.
Go to the Google API & Services Dashboard. In the top left corner, you'll see the project that you're currently in. Click on the little arrow to bring up the projects list, then click "New Project".
Fill in your desired project name and click "Create". Once it's done, click to view the project.
On the page, click the top left hamburger menu. Navigate to "APIs & Services -> OAuth consent screen".
Choose the type that best describes your application, then click "Create". For this tutorial, I'll be using the "external" type.
This page is where you can limit users to logging in only with their authorized emails. For example, if your organization's domain is "company.com", Google would only allow users to login with their "name@company.com" emails. Pretty cool, right?
On this page, enter in your application requirements. The application name and logo will show when the user logs in to the API, so pick something that you're happy with. You must enter a support email as well. I chose to leave the scopes on their default, as all the API we're building needs is their Google email and profile. I left the rest of the settings on their default for this tutorial and clicked "save".
Click on "Credentials" in the sidebar. Then, click "Create Credentials" at the top and select "OAuth client ID".
Select "Web application" for the type. Fill in the rest of the settings, then click "Create". "Authorized JavaScript origins" limit what sites can forward the user to the Google login, and "authorized redirect URIs" are where Google forwards the user back to after logging in. For this tutorial, I will be developing on my machine and running the application on port 8000 (thus, http://localhost:8000) and using the auth
endpoint to handle the Google -> FastAPI application transaction.
You're just about done setting up OAuth! Once the client is created, copy the client ID and client secret into your .env file. The FastAPI application we're about to build will read this file for the necessary information to handle OAuth. If you're confused about the file structure, check the tutorial repo.
At the top of our Python file, we have our imports.
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from starlette.config import Config
from starlette.requests import Request
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
from authlib.integrations.starlette_client import OAuth
The first Optional
and FastAPI
imports are standard among FastAPI applications. I want to explicitly call out Depends
and HTTPException
, which will be used to validate a user's login credentials prior to an API call.
The Starlette
and AuthLib
imports are the key to our Google OAuth implementation. They'll handle all of the heavy lifting, specifically the redirect URI generation and callback handling. Props to their developers as they make our implementation a breeze!
Here we initialize our FastAPI
instance with a root homepage. I've disabled the default generated documentation because I plan to protect it behind our new user authentication layer later. If the user is not logged in, the pay will display a login link. If the user is logged in, the page will display their email address as well as links to the documentation and logout.
app = FastAPI(docs_url=None, redoc_url=None)
app.add_middleware(SessionMiddleware, secret_key='!secret')
@app.get('/')
async def home(request: Request):
user = request.session.get('user')
if user is not None:
email = user['email']
html = (
f'<pre>Email: {email}</pre><br>'
'<a href="/docs">documentation</a><br>'
'<a href="/logout">logout</a>'
)
return HTMLResponse(html)
return HTMLResponse('<a href="/login">login</a>')
Rather than saving our authentication to a cookie manually, I've opted to use Starlettes's SessionMiddleware
. I specified a secret key that will be used to encode/decode the cookie. In actual applications, I highly recommended using a more secure key and saving it to an environment/config file. Hardcoding secrets, keys, database credentials, etc. in your files is both bad practice and a security risk.
FastAPI has good documentation on middleware, and the key thing to note is that it can perform operations on requests prior to being passed through to the rest of the application as well before delivering responses. Starlette's SessionMiddleware
documentation explains how it utilizes this technique to add a signed cookie to the user session that is readable but not modifiable. Still confused about what middleware is? Check out the diagram below.
With AuthLib
, it doesn't take much work to implement Google OAuth into our FastAPI application. After loading our client ID and client secret from our configuration file, we register it with the following scopes:
-
openid
: required by Google's OpenID Connect API -
email
: grants the application access to the user's email address -
profile
: grants the applicattion acess to the user's profile, such as name and profile picture link
For more information on the allowed scopes, please check out Google's documentation.
# Initialize our OAuth instance from the client ID and client secret specified in our .env file
config = Config('.env')
oauth = OAuth(config)
CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration'
oauth.register(
name='google',
server_metadata_url=CONF_URL,
client_kwargs={
'scope': 'openid email profile'
}
)
@app.get('/login', tags=['authentication']) # Tag it as "authentication" for our docs
async def login(request: Request):
# Redirect Google OAuth back to our application
redirect_uri = request.url_for('auth')
return await oauth.google.authorize_redirect(request, redirect_uri)
@app.route('/auth')
async def auth(request: Request):
# Perform Google OAuth
token = await oauth.google.authorize_access_token(request)
user = await oauth.google.parse_id_token(request, token)
# Save the user
request.session['user'] = dict(user)
return RedirectResponse(url='/')
@app.get('/logout', tags=['authentication']) # Tag it as "authentication" for our docs
async def logout(request: Request):
# Remove the user
request.session.pop('user', None)
return RedirectResponse(url='/')
With our OAuth registered, we next define three endpoints: login
, auth
, and logout
. They do exactly what their names suggest. login
redirects to Google's sign-in page and then back to our FastAPI application once the user is logged in. Next, auth
handles this incoming user access token and saves the user to a cookie thanks to our SessionMiddleware
from earlier. Lastly, logout
clears the user cookie from the session and resets our application back to the start.
At this point, we have functioning Google OAuth in our FastAPI application! By starting up our FastAPI application and heading to http://localhost:8000/ (this is the default URL from the uvicorn
library; see their documentation), we should see the following:
Clicking the login link will take us to the Google OAuth sigin-in page.
After signing in, we'll be redirected back to our application. Notice that the home page now shows the documentation link as well as the the logout link. If you click the logout link, you'll be redirected back to where we started. If you click the documentation link, you'll get a dead link.
That's because after disabling FastAPI's autogenerated documentation, we never reimplemented it in our application! Let's fix that, protecting it with an authentication layer while we're at it.
To implement our documentation, we'll copy the structure of the autogenerated FastAPI documenation while making use of the Depends
class. Thanks to FastAPI, we can write a single function that will check for an authenticated user before displaying the documentation. If no user is logged in, it will throw an HTTP 403 error. For more information on FastAPI dependencies, check out the documentation.
# Try to get the logged in user
async def get_user(request: Request) -> Optional[dict]:
user = request.session.get('user')
if user is not None:
return user
else:
raise HTTPException(status_code=403, detail='Could not validate credentials.')
return None
@app.route('/openapi.json')
async def get_open_api_endpoint(request: Request, user: Optional[dict] = Depends(get_user)): # This dependency protects our endpoint!
response = JSONResponse(get_openapi(title='FastAPI', version=1, routes=app.routes))
return response
@app.get('/docs', tags=['documentation']) # Tag it as "documentation" for our docs
async def get_documentation(request: Request, user: Optional[dict] = Depends(get_user)): # This dependency protects our endpoint!
response = get_swagger_ui_html(openapi_url='/openapi.json', title='Documentation')
return response
Check this out: if we restart our API and navigate to the documentation page again (note: you may need to log in again if your cookie expired), you'll see this awesome documentation page generated by FastAPI.
To illustrate that it's protected, go back to the homepage and logout. Then, manually type in the documentation URL. You should get the HTTP 403 error that we defined.
And that's it! We successfuly created a FastAPI application with an Google OAuth authentication layer wrapped on top of it!