In this blog post, you will learn how to create a Python app using Flask and the Google API which will:

  • Support Google Authentication with Python and Flask
  • Restrict access via an OAuth scope, so that the app can only view and manage Google Drive files and folders which were created by the app
  • Read and write files on the user’s Google Drive with Python.

By the time you get to the end of this blog post, you’ll have built a basic Google Drive file browser which looks something like:

OAuth Google Drive Scope

This blog post is divided up into sections which progressively build up an app which interacts with the user’s Google Drive. If you only want to find out about how to do user Authentication with Google and Python, feel free to stop there.

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.

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

Google Authentication with Python and Flask

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.

Add a file with requirements.txt with the following:

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

Install the packages in requirements.txt via the following command:

pip install -r requirements.txt

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'

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

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

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():
    req_state = flask.request.args.get('state', default=None, type=None)

    if req_state != flask.session[AUTH_STATE_KEY]:
        response = flask.make_response('Invalid state parameter', 401)
        return response
    
    session = OAuth2Session(CLIENT_ID, CLIENT_SECRET,
                            scope=AUTHORIZATION_SCOPE,
                            state=flask.session[AUTH_STATE_KEY],
                            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)

    return flask.redirect(BASE_URI, code=302)

This contains all the code that we need to authenticate with Google via the Google Login API and fetch information about the current user.

Some highlights and points of note:

To use the google_auth module, create a file called app.py containing the following:

import functools
import json
import os

import flask

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

import google_auth

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

app.register_blueprint(google_auth.app)

@app.route('/')
def index():
    if google_auth.is_logged_in():
        user_info = google_auth.get_user_info()
        return '<div>You are currently logged in as ' + user_info['given_name'] + '<div><pre>' + json.dumps(user_info, indent=4) + "</pre>"

    return 'You are not currently logged in.'

Create a script to start 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=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=app.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 which 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.

Note how the page says that “Google will share your name, email address and profile picture” - this is because we specified the email and profile scopes in AUTHORIZATION_SCOPE.

Sign in with Google

Select an account to sign with.

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

Now we have a Python app which supports Google Authentication - if that’s all you need, feel free to stop here. Otherwise, continue reading to learn about how to connect our app to the user’s Google Drive.

Adding support for Google Drive OAuth scopes

Now let’s enable support for the Google Drive scope which allows our app to read and write files on Google Drive which were created by our app.

Navigate to the OAuth consent screen in the Google API console:

OAuth Consent Setup

Scroll down to the Scopes for Google APIs and add the ../auth/drive.file scope:

Add Google Drive Scope

Your Scopes for Google APIs should now look like this:

OAuth Google Drive Scope

In google_auth.py, update AUTHORIZATION_SCOPE to include the Google Drive scope. It should look something like:

AUTHORIZATION_SCOPE ='openid email profile https://www.googleapis.com/auth/drive.file'

Now, if you log into the app, you’ll see a second consent screen which prompts the user to allow the app to access to Google Drive:

OAuth Google Drive Scope

Now that we’ve updated the Google API settings to support the drive.file scope, and have updated our app to request the drive.file scope on login, we can move on to setting up the app to read from and write to the user’s Google Drive.

Calling the Google Drive API with Python

Add a file called google_drive.py with the following:

import io
import tempfile

import flask

from apiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
import googleapiclient.discovery
from google_auth import build_credentials, get_user_info

from werkzeug.utils import secure_filename

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

def build_drive_api_v3():
    credentials = build_credentials()
    return googleapiclient.discovery.build('drive', 'v3', credentials=credentials).files()

We’re also importing the google_auth module which we created earlier:

  • build_credentials will be important for accessing the user’s Google Drive
  • get_user_info will be used to display information about the current user on the page.

The build_drive_api_v3() method creates a Drive API files instance which will be used whenever we need to create/read/update files in the user’s Google Drive.

Import google_drive in app.py:

import google_drive

Register the google_drive blueprint by adding the following to app.py:

app.register_blueprint(google_drive.app)

Flask template for the Google Drive list view

Next, we’ll start building the Flask Template for the view that you saw in the screenshot at the beginning of this post.

In the code below, we’re using the Bootstrap starter template as the foundation, then will be adding:

  • A nav header displaying the users given name and a logout button
  • A form so that you can upload new files
  • A list of files that have been previously uploaded
  • Links next to each file, so that they can be viewed in the app

Update the index method in app.py so that it looks like:

@app.route('/')
def index():
    if google_auth.is_logged_in():
        return flask.render_template('list.html', user_info=google_auth.get_user_info())

    return 'You are not currently logged in.'

Add the following modified Bootstrap starter template to a templates/list.html - this will be rendered via the Flask render_template call above.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <title>Google Drive File Browser</title>
  </head>
  <body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <div class="container">
          <a class="navbar-brand" href="#">Google Drive File Browser</a>
          <ul class="navbar-nav mr-auto"></ul>
          <span class="navbar-text mr-2">
            Currently logged in as {{ user_info['given_name'] }}
          </span>
          <a href="/google/logout" class="btn btn-primary" role="button" aria-pressed="true">Logout</a>
      </div>
    </nav>
    <div class="container">   
        <!-- We'll add code here in the next couple of sections -->
    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    <!-- Add additional script blocks here -->
  </body>
</html>

The only modification to the Bootstrap starter template is the nav section, which contains the Logout button, and will be used to display the current user’s first name.

Creating files in Google Drive with Python

Add the following save_image method to the end of google_drive.py:

def save_image(file_name, mime_type, file_data):
    drive_api = build_drive_api_v3()

    generate_ids_result = drive_api.generateIds(count=1).execute()
    file_id = generate_ids_result['ids'][0]

    body = {
        'id': file_id,
        'name': file_name,
        'mimeType': mime_type,
    }

    media_body = MediaIoBaseUpload(file_data,
                                   mimetype=mime_type,
                                   resumable=True)

    drive_api.create(body=body,
                     media_body=media_body,
                     fields='id,name,mimeType,createdTime,modifiedTime').execute()

    return file_id

This is the code that we need to upload to Google Drive.

We’re using:

  • The Drive API generateIds method to create the file_id used in the create request, and will be used to fetch files toward the end of this blog post.
  • MediaIoBaseUpload to instantiate media_body. MediaIoBaseUpload used the file_data object, which is a standard Python file object.
  • The Drive API create method to upload the file to Google Drive

Post Multipart Form Data to a Python Flask endpoint

Add this Flask API endpoint to which will recieve the uploaded file data to google_drive.py:

@app.route('/gdrive/upload', methods=['GET', 'POST'])
def upload_file():
    if 'file' not in flask.request.files:
        return flask.redirect('/')

    file = flask.request.files['file']
    if (not file):
        return flask.redirect('/')
        
    filename = secure_filename(file.filename)

    fp = tempfile.TemporaryFile()
    ch = file.read()
    fp.write(ch)
    fp.seek(0)

    mime_type = flask.request.headers['Content-Type']
    save_image(filename, mime_type, fp)

    return flask.redirect('/')

In the code above we’re:

  • Fetching the uploaded file with name ‘file’ from flask.request.files - we’ll need to specify name=file in the upload form too
  • Calling Flask’s secure_filename method, which is generally just good practice to help sanitise any input from the user
  • Saving the image to Google Drive via the save_image method that we created earlier

Add the following to template/list.html, within the container div:

<h3>Upload a new file</h3>
<form method='POST' enctype='multipart/form-data' action="/gdrive/upload">
  <div class="input-group mb-3">
    <div class="custom-file">
      <input type="file" name="file" class="custom-file-input" id="inputGroupFile01" aria-describedby="inputGroupFileAddon01">
      <label class="custom-file-label" for="inputGroupFile01">Choose file</label>
    </div>
    <div class="input-group-append">
      <input class="input-group-text" type="submit" value="Upload" />
    </div>
  </div>
</form>

Add this script block after the jQuery script block in template/list.html:

<script>
    $('.custom-file-input').on('change', function() { 
        let fileName = $(this).val().split('\\').pop(); 
        $(this).next('.custom-file-label').addClass("selected").html(fileName); 
    });
</script>

Start the app, and navigate to http://localhost:8040 - you should see the upload form, and be able to upload a file to your Google drive.

If you get an error such as:

Insufficient Permission: Request had insufficient authentication scopes.

It means that you’re still logged in with a session using the old scopes (openid email profile) and are missing the Google Drive scope. In order to get a session which includes the Google Drive scope that you added earlier, you need to log out and log in again.

After uploading the file, navigate over to https://drive.google.com - you should see the new file in your Google Drive.

Getting a list of files in Google Drive with Python

Currently our app only supports uploading of files to Googe Drive, but no way to get a list of previously uploaded files, or viewing individual files

In this step, we’ll add support for listing files that were previously uploaded by our app.

In app.py, update the index() method so that it looks like this:

@app.route('/')
def index():
    if google_auth.is_logged_in():
        drive_fields = "files(id,name,mimeType,createdTime,modifiedTime,shared,webContentLink)"
        items = google_drive.build_drive_api_v3().list(
                        pageSize=20, orderBy="folder", q='trashed=false',
                        fields=drive_fields
                    ).execute()

        return flask.render_template('list.html', files=items['files'], user_info=google_auth.get_user_info())

    return 'You are not currently logged in.'

In this update, we’re fetching files from Google drive via a call to list, and using the fields parameter to specify the data that we want to fetch.

The fields parameter took me a while to figure out. The V2 version of the Google API provided data for createdTime, webContentLink, etc by default, whereas the V3 doesn’t - you must use the fields parameter to specify the fields that you want returned in the API response.

Confusingly, you won’t see it mentioned in the docs for the Google Drive API list method method, however the fields parameter is documented in the:

Displaying the list of files in Google Drive

Now that we have our /gdrive endpoint, we’ll set up the Flask template to display the list of recently uploaded files.

In template/list.html, add a section to list files which were recently uploaded by our app to Google Drive.

Directly below the upload form which you added above, add the following:

<h3>Recent Files</h3>
<table class="table">
<thead>
  <tr>
    <th scope="col">Name</th>
    <th scope="col">Mime Type</th>
    <th scope="col">Created Time</th>
    <th scope="col"></th>
  </tr>
</thead>
<tbody>
  {% for file in files %}
  <tr>
    <td>{{ file['name'] }}</td>
    <td>{{ file['mimeType'] }}</td>
    <td>{{ file['createdTime'] }}</td>
    <td><a href="/gdrive/view/{{file['id']}}">View</a></td>
  </tr>
  {% endfor %}
</tbody>
</table>

This iterates over the files that we specified as part of flask.render_template, and displays them one-by-one, along with a link to view the file. The View link won’t work just yet. We’ll set that up in the next step.

Downloading files from Google Drive with Python

Now that we’ve added support for uploading files, and displaying a list of previously uploaded files, we’ll add support for viewing an individual uploaded file.

Add a file called google_drive.py with the following:

@app.route('/gdrive/view/<file_id>', methods=['GET'])
def view_file(file_id):
    drive_api = build_drive_api_v3()

    metadata = drive_api.get(fields="name,mimeType", fileId=file_id).execute()

    request = drive_api.get_media(fileId=file_id)
    fh = io.BytesIO()
    downloader = MediaIoBaseDownload(fh, request)

    done = False
    while done is False:
        status, done = downloader.next_chunk()

    fh.seek(0)

    return flask.send_file(
                     fh,
                     attachment_filename=metadata['name'],
                     mimetype=metadata['mimeType']
               )

This is the endpoint called by the link to /gdrive/view/{{file[‘id’]}} in the table which you added in the section above.

Flask’s send_file() method is used to

You’re done! Start the app and test it out

Start up the app via run.bat, navigate to http://localhost:8040/gdrive and you should see the Google Drive File Browser that we’ve created.

In the screenshot below, you can see that I’ve already uploaded happy-doge.jpg to my Google Drive:

OAuth Google Drive Scope

Try uploading some image files, and viewing them via the ‘View’ link. The page that you see should display the image that you’ve uploaded.

Here’s what it looks like after I click the View link on happy-doge.jpg:

An uploaded file

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.
  • How to insert and delete files from a user’s Google Drive

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:

Thanks for reading!

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