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

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

Setting up

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 create a new virtual environment:

pip install virtualenv
virtualenv venv

If you’re using the bash shell, activate the virtual environment with the following:

venv/Scripts/activate

If you’re using Windows, activate the virtual environment with the following:

venv\Scripts\activate.bat

Now that the virtual environment has been installed and activated, install the packages in requirements.txt:

pip install -r requirements.txt

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 containing the following:

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'

AUTHORIZATION_SCOPE ='openid email profile'

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.Flask(__name__)
app.secret_key = os.environ.get("FN_FLASK_SECRET_KEY", default=False)

@app.route('/')
def index():
    if is_logged_in():
        user_info = get_user_info()
        return 'You are currently logged in as ' + user_info['given_name']

    return 'You are not currently logged in'

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()

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.

Starting the Flask app

Now 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=google_auth.py
export FLASK_DEBUG=1
export FN_FLASK_SECRET_KEY=SOMETHING RANDOM AND SECRET

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=google_auth.py
set FLASK_DEBUG=1
set FN_FLASK_SECRET_KEY=SOMETHING RANDOM AND SECRET

python -m flask run -p 8040

In the scripts above:

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.

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

OAuth not logged in

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.

Logging in with Google

With the flask app up and running, 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 something 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

The code is available on GitHub

If you’re stuck, or just want the code, check out: https://github.com/mattbutton/google-authentication-with-python-and-flask

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.

I’m always keen for feedback and support, so if you’re keen for me to get part 2 out sooner, please leave a comment below!

Thanks for reading!

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