This blog post is part 1 of a multi-part series, and covers the basics of getting Google authentication to work with Python and Flask. Part 2 will cover actually reading and writing the files to Google Drive.

In this blog post, you will learn how to:
  • Create a Python Flask app, which allows a user to log in with their Google account
  • Request authorization for the app to read and write files in the user’s Google Drive.
Some background:

I was recently working on an app using Python and Flask, and wanted to support Google authentication and read/write access to Google Drive.

I wanted to answer the following questions:

  • How do I support Google Authentication with Python and Flask?
  • How do I read and write to the authenticated user’s Google Drive?
  • How to I restrict access to view and manage Google Drive files and folders that were created with the app?
The Google API docs are confusing!

Getting Google authentication and authorization set up was quite a confusing process; there are lots of scattered, confusing, obsolete, or generally misleading docs on how to integrate Python with Google Drive. For example:

Disclaimer:
  • Security is a big topic, and the advice this blog post doesn’t come with any warranty or guarantees. This blog post is intended as a “getting started” article, and does not provide comprehensive security advice.

Prerequisites

Make sure you have the following before you start:

Set up a new Google OAuth client ID

Navigate to the Google API console and select the option to create a new set of OAuth credentials:

Create OAuth credentials

Then set up the OAuth redirect URI. For this app, we’ll be running on localhost, port 8040, and redirecting to /google/auth, so our redirect URI will be http://localhost:8040/google/auth.

Set OAuth authorized redirect URI

Once you’re done, click Create, and you’ll be presented with a dialog with our OAuth Client ID and Client Secret. Copy these, and store them somewhere safe. We’ll use them later.

Set OAuth authorized redirect URI

Create a new Flask app

We’ll set this app up in a virtual environment.

In a new folder, add a new file requirements.txt containing the following:

authlib==0.10
flask==1.0.2
google-api-python-client
google-auth
virtualenv

Then in the terminal, execute the following in the app root directory to create the virtual environment:

pip install virtualenv
virtualenv venv
pip install -r requirements.txt

For future sessions, the virtual environment can be re-activated via the executing the following command in the app root directory:

If you’re using the bash shell:

venv/Scripts/activate

If you’re using Windows:

venv\Scripts\activate

Then we’ll set up the scripts to execute the app.

If you’re using the bash shell, create a run.sh file at your project root that looks like:

export FN_AUTH_REDIRECT_URI=http://localhost:8040/google/auth
export FN_BASE_URI=http://localhost:8040
export FN_CLIENT_ID=THE CLIENT ID WHICH YOU CREATED EARLIER
export FN_CLIENT_SECRET=THE CLIENT SECRET WHICH YOU CREATED EARLIER

export FLASK_APP=app.py
export FLASK_DEBUG=1
export FN_FLASK_SECRET_KEY=1234567

python -m flask run -p 8040

If you’re using Windows, create a run.bat file that looks like:

set FN_AUTH_REDIRECT_URI=http://localhost:8040/google/auth
set FN_BASE_URI=http://localhost:8040
set FN_CLIENT_ID=THE CLIENT ID WHICH YOU CREATED EARLIER
set FN_CLIENT_SECRET=THE CLIENT SECRET WHICH YOU CREATED EARLIER

set FLASK_APP=app.py
set FLASK_DEBUG=1
set FN_FLASK_SECRET_KEY=1234567

python -m flask run -p 8040

FN_AUTH_REDIRECT_URI should be the OAuth redirect URI you set up earlier

FN_CLIENT_ID Should be set to the Google OAuth client id which you saved earlier

FN_CLIENT_SECRET Should be set to the Google OAuth client secret which you saved earlier

FN_FLASK_SECRET_KEY should be a random value. This will be used for encrypting the cookie in the Flask session.

It’s important to keep these values secret. Do not check them into source control.

Create app.py containing the following:

import functools
import os
import tempfile
import urllib

import flask
from flask import Flask, request

import googleapiclient.discovery

app = Flask(__name__)
app.secret_key = os.environ.get("FN_FLASK_SECRET_KEY", default=False)

@app.route('/')
def index():
    return 'Hello, World!'

Start Flask via either run.bat or run.sh, and in your browser, navigate to http://localhost:8040/hello. If everything is working correctly, you should see something that looks like the following:

Flask Hello World

If you get any errors in the above process, there’s more info on setting up a Flask app is in the Flask Quickstart. Otherwise, leave a comment below.

Now, moving on to integrating with Google Authentication.

Google Authentication

We’ll be using Authlib as an alternative to the deprecated oauth2client. For this example, there’s no special reason to use Authlib instead of google-auth; the only reason I used Authlib is because I found the Authlib documentation easier to follow than google-auth.

Create a file called google_auth.py

import functools
import os

import flask

from authlib.client import OAuth2Session
import google.oauth2.credentials
import googleapiclient.discovery

ACCESS_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
AUTHORIZATION_URL = 'https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent'

# Google Auth scope for access to 
AUTHORIZATION_SCOPE ='openid email profile https://www.googleapis.com/auth/drive.file'

AUTH_REDIRECT_URI = os.environ.get("FN_AUTH_REDIRECT_URI", default=False)
BASE_URI = os.environ.get("FN_BASE_URI", default=False)
CLIENT_ID = os.environ.get("FN_CLIENT_ID", default=False)
CLIENT_SECRET = os.environ.get("FN_CLIENT_SECRET", default=False)

AUTH_TOKEN_KEY = 'auth_token'
AUTH_STATE_KEY = 'auth_state'
USER_INFO_KEY = 'user_info'

app = flask.Blueprint('google_auth', __name__)

def no_cache(view):
    @functools.wraps(view)
    def no_cache_impl(*args, **kwargs):
        response = flask.make_response(view(*args, **kwargs))
        response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Expires'] = '-1'
        return response

    return functools.update_wrapper(no_cache_impl, view)

@app.route('/google/login')
@no_cache
def login():
    session = OAuth2Session(CLIENT_ID, CLIENT_SECRET, scope=AUTHORIZATION_SCOPE, redirect_uri=AUTH_REDIRECT_URI)
    uri, state = session.authorization_url(AUTHORIZATION_URL)
    flask.session[AUTH_STATE_KEY] = state
    flask.session.permanent = True
    return flask.redirect(uri, code=302)


@app.route('/google/auth')
@no_cache
def google_auth_redirect():
    state = flask.request.args.get('state', default=None, type=None)
    
    session = OAuth2Session(CLIENT_ID, CLIENT_SECRET, scope=AUTHORIZATION_SCOPE, state=state, redirect_uri=AUTH_REDIRECT_URI)
    oauth2_tokens = session.fetch_access_token(ACCESS_TOKEN_URI, authorization_response=flask.request.url)
    flask.session[AUTH_TOKEN_KEY] = oauth2_tokens

    return flask.redirect(BASE_URI, code=302)

@app.route('/google/logout')
@no_cache
def logout():
    flask.session.pop(AUTH_TOKEN_KEY, None)
    flask.session.pop(AUTH_STATE_KEY, None)
    flask.session.pop(USER_INFO_KEY, None)

    return flask.redirect(BASE_URI, code=302)

def is_logged_in():
    return True if AUTH_TOKEN_KEY in flask.session else False

def build_credentials():
    if not is_logged_in():
        raise Exception('User must be logged in')

    oauth2_tokens = flask.session[AUTH_TOKEN_KEY]
    return google.oauth2.credentials.Credentials(
        oauth2_tokens['access_token'],
        refresh_token=oauth2_tokens['refresh_token'],
        client_id=CLIENT_ID,
        client_secret=CLIENT_SECRET,
        token_uri=ACCESS_TOKEN_URI)

def get_user_info():
    credentials = build_credentials()
    oauth2_client = googleapiclient.discovery.build('oauth2', 'v2', credentials=credentials)
    return oauth2_client.userinfo().get().execute()

The code above contains our basic login/logout endpoints.

Some higlights and points of note:

  • We’re fetching FN_CLIENT_ID, FN_CLIENT_SECRET, etc from environment variables via os.environ.get
  • The OAuth 2.0 Scope in AUTHORIZATION_SCOPE contains https://www.googleapis.com/auth/drive.file, which means the scope is limited to: View and manage Google Drive files and folders that you have opened or created with this app.
  • We’re preventing browser caching of responses from the login/logout endpoints via a custom no_cache function decorator
  • In google_auth_redirect, we’re storing the OAuth state parameter in the Flask session using AUTH_STATE_KEY
  • get_user_info, which performs a request to get Google user profile info, and returns a dictionary containing the data.

Restart the Flask app, and navigate to http://localhost:8040/google/login, and you should be redirected to the Sign in with Google screen which looks something like the screenshot below:

Sign in with Google

Select an account to sign with, and click Allow on the Accept screen that follows. Note on the Allow screen, the permission being requested reflects the OAuth scope requested earlier:

OAuth consent screen

Once you have successfully logged in, you should see a screen that looks like:

OAuth login successful

To log out, navigate to http://localhost:8040/google/logout. You should see a screen that looks like:

OAuth not logged in

Where to next?

In this blog post, I’ve covered the basics of getting Google authentication to work with Python and Flask.

Security is a big topic, and the advice this blog post doesn’t come with any warranty or guarantees. This blog post is intended as a “getting started” article, and does not provide comprehensive security advice.

If you’re planning on hosting your app in production, there are some additional security considerations to be made. Among other things:

I’ll be working on part 2 in the near future, which will cover reading from, and writing to Google Drive.

Thanks for reading!

If you like this blog post, have any feedback, or any questions, please get in touch, or leave a comment below.