TECH

Check If Google Credentials Are Valid

JARRETT RETZ September 9th, 2021 programming python fastapi credentials google authorization apis

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:

https://stackoverflow.com/questions/27771324/google-api-getting-credentials-from-refresh-token-with-oauth2client-client

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.


Have a thought about the article?

Send JRTS a message!

We'll use this email to respond to your message.
Contact