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
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.
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:
If you’re using Windows, activate the virtual environment with the following:
Now that the virtual environment has been installed and activated, install the packages in requirements.txt:
pip install -r requirements.txt
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:
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:
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 something like:
To log out, navigate to http://localhost:8040/google/logout. You should see a screen that looks like:
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:
- 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.
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.