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.
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:
- The Google Drive V3 Python Quickstart has an example which uses oauth2client. After spending a fair amount of time trying to get it working, I was surprised to find that oauth2client is deprecated, and not recommended for use.
- 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.
Make sure you have the following before you start:
- A Google account
- The Anaconda distribution of Python
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:
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.
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.
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:
If you’re using Windows:
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:
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.
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:
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:
Once you have successfully logged in, you should see a screen that looks like:
To log out, navigate to http://localhost:8040/google/logout. You should see a screen that looks like:
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:
- Use HTTPS
- Read over the Flask Security Considerations especially the part about Cross-Site Request Forgery (CSRF) and Set-Cookie options
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.