Authenticating Users with Python

This part of the Python Bookshelf tutorial shows how to create a sign-in flow for users and how to use profile information to provide users with personalized functionality.

By using Google Identity Platform, you can easily access information about your users while ensuring their sign-in credentials are safely managed by Google. OAuth 2.0 makes it easy to provide a sign-in flow for all users of your app and provides your application with access to basic profile information about authenticated users.

This page is part of a multi-page tutorial. To start from the beginning and read the setup instructions, go to Python Bookshelf app.

Creating a web application client ID

A web app client ID lets your app authorize users and access Google APIs on behalf of your users.

  1. In the Google Cloud Platform Console, go to Credentials.

    Credentials.

  2. Click OAuth consent screen. For the product name, enter Python Bookshelf App.

  3. For Authorized Domains, add your App Engine app name as [YOUR_PROJECT_ID].appspot.com. Replace [YOUR_PROJECT_ID] with your GCP project ID.

  4. Fill in any other relevant, optional fields. Click Save.

  5. Click Create credentials > OAuth client ID.

  6. In the Application type drop-down list, click Web Application.

  7. In the Name field, enter Python Bookshelf Client.

  8. In the Authorized redirect URIs field, enter the following URLs, one at a time.

    http://localhost:8080/oauth2callback
    http://[YOUR_PROJECT_ID].appspot.com/oauth2callback
    https://[YOUR_PROJECT_ID].appspot.com/oauth2callback

  9. Click Create.

  10. Copy the client ID and client secret and save them for later use.

Configuring settings

This section uses code in the 4-auth directory. Edit the files and run commands in this directory.

  1. Open config.py for editing.
  2. Set the value of PROJECT_ID to your project ID, which is visible in the GCP Console.
  3. Set DATA_BACKEND to the same value you used during the Using Structured Data tutorial.
  4. If you are using Cloud SQL or MongoDB, set the values under the Cloud SQL or Mongo section to the same values you used during the Using Structured Data step.
  5. Set the value CLOUD_STORAGE_BUCKET to your Cloud Storage bucket name.

  6. Under the OAuth2 configuration section, set the values of GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET to the application client ID and secret that you created previously.

  7. Save and close config.py.

If you are using Cloud SQL:

  1. Open app.yaml for editing.
  2. Set the value of cloud_sql_instances to the same value used for CLOUDSQL_CONNECTION_NAME in config.py . It should be in the format project:region:cloudsql-instance. Uncomment this entire line.
  3. Save and close app.yaml.

Installing dependencies

Enter these commands to create a virtual environment and install dependencies:

Linux/macOS

virtualenv -p python3 env
source env/bin/activate
pip install -r requirements.txt

Windows

virtualenv -p python3 env
env\scripts\activate
pip install -r requirements.txt

Running the app on your local machine:

  1. Start a local web server:

    python main.py
    
  2. In your web browser, enter this address:

    http://localhost:8080

Now you can browse the app's web pages, sign in using your Google account, add books, and see the books you've added using the My Books link in the top navigation bar.

Press Control+C to exit the worker and then the local web server.

Deploying the app to the App Engine flexible environment

  1. Deploy the sample app:

    gcloud app deploy
    
  2. In your web browser, enter this address. Replace [YOUR_PROJECT_ID] with your project ID:

    https://[YOUR_PROJECT_ID].appspot.com
    

If you update your app, you can deploy the updated version by entering the same command you used to deploy the app the first time. The new deployment creates a new version of your app and promotes it to the default version. The older versions of your app remain, as do their associated VM instances. Be aware that all of these app versions and VM instances are billable resources.

You can reduce costs by deleting the non-default versions of your app.

To delete an app version:

  1. In the GCP Console, go to the App Engine Versions page.

    Go to the Versions page

  2. Click the checkbox next to the non-default app version you want to delete.
  3. Click Delete delete at the top of the page to delete the app version.

For complete information about cleaning up billable resources, see the Cleaning up section in the final step of this tutorial.

Application structure

The following diagram shows the application's components and how they connect to one another.

Auth sample structure

The application redirects the user to Google's authentication service, which redirects the user back when authorized. The application stores the user's profile information in the session.

Understanding the code

This section walks you through the application code and explains how it works.

About sessions

Before your application can authenticate users, you will need a way to store information about the current user in a session. Flask, which includes sessions backed by encrypted cookies, provides this capability.

To use sessions with Flask you must configure a secret key for your application.

SECRET_KEY = 'secret'

Flask recommends using urandom to generate a random key.

python
>>> import os
>>> os.urandom(24)
'\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O<!\xd5\xa2\xa0\x9fR"\xa1\xa8'

Copy and paste the final output into config.py as your secret key.

In production, you may want use a centralized database to store sessions securely. Google Cloud Platform makes it easy to deploy many databases, including Redis or Memcache. Setting up these services is outside of the scope of this tutorial.

Authenticating users

Authenticating the user involves two basic steps, which together are called the web service flow:

  • Redirect the user to Google’s authorization service.
  • Process the response when Google redirects the user back to your application.

The flask_util extension in oauth2client makes it easy to integrate OAuth2 into your application without having to implement the flow yourself.

  1. Create a UserOAuth2 instance.

    from oauth2client.contrib.flask_util import UserOAuth2
    
    
    oauth2 = UserOAuth2()
  2. Initialize the instance with the Flask application. The scopes email and profile give us access to the user's email address and basic Google profile info.

    # Initalize the OAuth2 helper.
    oauth2.init_app(
        app,
        scopes=['email', 'profile'],
        authorize_callback=_request_user_info)
  3. Create a way for users to log out, as flask_util does not provide this by default.

    # Add a logout handler.
    @app.route('/logout')
    def logout():
        # Delete the user's profile and the credentials stored by oauth2.
        del session['profile']
        session.modified = True
        oauth2.storage.delete()
        return redirect(request.referrer or '/')
  4. After you obtain credentials, you can fetch information about the user. The credentials include an id_token that will give you the user’s ID and email address, but it would be more useful to obtain some basic profile information such as the user's name and photo. The Google OAuth2 API method userinfo can provide that for any authenticated user. UserOAuth2 has an authorize_callback argument that can be used to execute a function after credentials are obtained for the user.

    def _request_user_info(credentials):
        """
        Makes an HTTP request to the Google OAuth2 API to retrieve the user's basic
        profile information, including full name and photo, and stores it in the
        Flask session.
        """
        http = httplib2.Http()
        credentials.authorize(http)
        resp, content = http.request(
            'https://www.googleapis.com/oauth2/v3/userinfo')
    
        if resp.status != 200:
            current_app.logger.error(
                "Error while obtaining user profile: \n%s: %s", resp, content)
            return None
        session['profile'] = json.loads(content.decode('utf-8'))
    
  5. Use the profile information provided by the session in your templates to indicate to the user that they are logged in or logged out:

    <p class="navbar-text navbar-right">
    {% if session.profile %}
      <a href="/logout">
        {% if session.profile.picture %}
          <img class="img-circle" src="{{session.profile.picture}}" width="24">
        {% endif %}
        {{session.profile.name}}
      </a>
    {% else %}
      <a href="/oauth2authorize">Login</a>
    {% endif %}
    </p>

Personalization

Now that you have the logged-in user's information available via the session, you can keep track of which user added which book to the database.

@crud.route('/add', methods=['GET', 'POST'])
def add():
    if request.method == 'POST':
        data = request.form.to_dict(flat=True)

        # If an image was uploaded, update the data to point to the new image.
        image_url = upload_image_file(request.files.get('image'))

        if image_url:
            data['imageUrl'] = image_url

        # If the user is logged in, associate their profile with the new book.
        if 'profile' in session:
            data['createdBy'] = session['profile']['name']
            data['createdById'] = session['profile']['email']

        book = get_model().create(data)

        return redirect(url_for('.view', id=book['id']))

    return render_template("form.html", action="Add", book={})

Now that you have that information in the database, you can use it to create a new view that allows a user to see all the books that they have added.

@crud.route("/mine")
@oauth2.required
def list_mine():
    token = request.args.get('page_token', None)
    if token:
        token = token.encode('utf-8')

    books, next_page_token = get_model().list_by_user(
        user_id=session['profile']['email'],
        cursor=token)

    return render_template(
        "list.html",
        books=books,
        next_page_token=next_page_token)

Notice that this view is decorated with oauth2.required, which means that only users who have valid credentials will be able to access this view. If the user does not have credentials, they will be redirected through the OAuth2 web flow.

This code uses a new model method called list_by_user. The implementation depends on which database backend you chose.

Datastore

def list_by_user(user_id, limit=10, cursor=None):
    ds = get_client()
    query = ds.query(
        kind='Book',
        filters=[
            ('createdById', '=', user_id)
        ]
    )

    query_iterator = query.fetch(limit=limit, start_cursor=cursor)
    page = next(query_iterator.pages)

    entities = builtin_list(map(from_datastore, page))
    next_cursor = (
        query_iterator.next_page_token.decode('utf-8')
        if query_iterator.next_page_token else None)

    return entities, next_cursor

Cloud SQL

def list_by_user(user_id, limit=10, cursor=None):
    cursor = int(cursor) if cursor else 0
    query = (Book.query
             .filter_by(createdById=user_id)
             .order_by(Book.title)
             .limit(limit)
             .offset(cursor))
    books = builtin_list(map(from_sql, query.all()))
    next_page = cursor + limit if len(books) == limit else None
    return (books, next_page)

MongoDB

def list_by_user(user_id, limit=10, cursor=None):
    cursor = int(cursor) if cursor else 0

    results = mongo.db.books\
        .find({'createdById': user_id}, skip=cursor, limit=10)\
        .sort('title')
    books = builtin_list(map(from_mongo, results))

    next_page = cursor + limit if len(books) == limit else None
    return (books, next_page)
Was this page helpful? Let us know how we did:

Send feedback about...