Introduction
This is a short post on checking if Google OAuth2.0 credentials are valid in a Python—FastAPI—application.
The backstory is that I have an application that is in "test" mode in Google Cloud Projects. Therefore, the credentials expire every seven days, and I'm not sure when I'll get around to authorizing the application through Google's review process.
The application accesses my own personal data, so, at the moment, there is no need to make it scalable to every user on the internet.
However, the application is deployed, and I want to regrant access to the app from the app's dashboard when I need to. I don't want to go through the whole authorization process if the credentials are still valid.
So, I have a button that I can click, and:
- If my credentials are still good, don't send me through the authorization process.
- If they are no longer valid, send me through the authorization process
This was surprisingly hard to accomplish with almost a decade and a half of answers on the internet. Each trying to figure out OAuth and Google APIs as they evolve year-to-year. To conclude this long introduction, I have a way that might work (for now) that does what I need it to do.
After a lot of searching, the link to the SO post that helped the most is below:
Backstory continued
The application stores the google credentials alongside the user data in my database. That means I'm pulling the credentials from the database after authenticating the user.
How to Check if Google OAuth2 Credentials Are Still Valid
Create Button on Front End to Send Request to Back End
My front-end application uses React. The app's authentication is handled with JWTs and cookies. You can read more about the authentication process here.
// other dashboard code
<Button
variant="contained"
color="secondary"
role="link"
href={`${process.env.REACT_APP_URL}/sheets/oauth?token=${token}`}
>
Send Request
</Button>
// ...
The token
is the JWT (stored in memory) on the frontend. It is not a user OAuth2 token.
Try to Refresh Token
As far as I know, at this moment, there are no negative consequences for asking to refresh the token like this and then not doing anything after the refresh request.
I haven't used this very many times, but it currently allows me to continue to authorize requests.
# FastAPI application
# router.py
@googleRouter.get("/oauth")
def oauth(
request: Request, token: str = Query(..., description="User JWT access token.")
):
global token_table_name
global user_table_name
global admin_table_name
user_table = dynamodb.Table(user_table_name)
try:
# decode the JWT
# I'm sending the JWT in the URL instead of in
# the body
#
#
# The JWT expires after a few minutes
except JWTError as e:
raise credentials_exception
try:
# Get the user from the database
except Exception as e:
return e
# extract google credentials
google_token_creds = user.google_token
if len(google_token_creds.keys()) > 0:
# Check google auth status
# Hijacked the build_service function to return
# only credentials if auth_check is true
creds = build_service(user.username, auth_check=True)
# Try to refresh the token
try:
# If this fails, a RefreshError is thrown
#
# from google.auth.exceptions import RefreshError
#
#
# If it doesn't fail, then return a simple
# 200 status code and message
creds.refresh(google_auth_httplib2.Request(httplib2.Http()))
return JSONResponse(
status_code=200, content="User has already authorized google."
)
except RefreshError:
print('Credentials are no longer valid')
pass
# ...continue with the rest of the Google Authorization
# workflow
build_service
Function
For context, I'll provide the function for building the google discovery service that returns the credentials object.
def build_service(user_id, auth_check = False):
"""Creates Google API Service
Returns:
googleapiclient.discovery.Resource: Google API Service
"""
global user_table_name
user_table = dynamodb.Table(user_table_name)
try:
response = user_table.get_item(Key=dict(id=user_id))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
try:
user = User(**response["Item"])
token = user.google_token
except KeyError:
raise HTTPException(status_code=500, detail="Credentials item not found.")
creds = google.oauth2.credentials.Credentials(**token)
if auth_check:
return creds
service = build("sheets", "v4", credentials=creds)
return service
Conclusion
I made this change and wrote this post on the fly, so I have yet to sustain any downstream effects. Although for now, I think this will work as I continue to round out the application.