Integrate Google authentication with Python and Dash

Viewed 1330

I'm looking to incorporate Google authentication into a Dash app. I found a very helpful post on working with Flask here that I followed to set up the Google API access and get the authentication screen to appear allowing the user to login with their Google account. Handling the tokens I get in return is where I get lost. It's not clear to me how to access them or use them properly in further callbacks to get the user verified and logged in.

Ultimately, I need to check the verified email against a separate database before allowing access. That end of the pipeline is working, I'm just missing this authentication piece from Google. Below is a bare-bones example of what I'm trying to do. It will work if you have your API keys set up with Google.

import dash
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from flask import redirect, request
from flask_login import logout_user, current_user, LoginManager
import requests
import json
from oauthlib.oauth2 import WebApplicationClient
from flask_login import UserMixin
from sqlalchemy import (Table, 
    create_engine, 
    MetaData, 
    String,
    Integer, 
    DateTime, 
    Column)
from sqlalchemy.ext.declarative import declarative_base

import auth_utils

app = dash.Dash(__name__)

server = app.server

# Setup the login manager for the server
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = '/login'

# Set up User class for login manager
Base = declarative_base()

class User(Base, UserMixin):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String, unique=True)

@login_manager.user_loader
def load_user(user_id):
    return User.get(user_id)

# Get secret keys
GOOGLE_CLIENT_ID = 'your-keys-here'
GOOGLE_CLIENT_SECRET = 'and-here'
client = WebApplicationClient(GOOGLE_CLIENT_ID)

GOOGLE_DISCOVERY_URL = "https://accounts.google.com/.well-known/openid-configuration"

app.layout = html.Div([
    dcc.Location(id='base-url'),
    dcc.Location(id='login-url', refresh=True, pathname='/login'),
    html.Div(id='hidden-google-redirect-uri',
        style={'display': 'none'}),
    html.Button('Login with Google',
        id='google-login-button',
        n_clicks=0),
    html.Div(id='login-success-display')
])

@app.callback(
    Output('hidden-google-redirect-uri', 'children'),
    Input('google-login-button', 'n_clicks'),
)
def google_login(n_clicks):
    if n_clicks > 0:
        google_provider_cfg = requests.get(GOOGLE_DISCOVERY_URL).json()
        google_auth_endpoint = google_provider_cfg["authorization_endpoint"]

        request_uri = client.prepare_request_uri(
            google_auth_endpoint,
            redirect_uri=request.base_url + "/callback",
            scope=["openid", "email", "profile"]
        )
        return dcc.Location(href=request_uri, id='_hidden-google-redirect-uri')
    
    else:
        return html.Div()

@app.callback(
    Output('login-success-display', 'children'),
    Input('google-login-button', 'n_clicks'),
    State('hidden-google-redirect-uri', 'children')
)
def google_response(n_clicks, google_response):
    if n_clicks == 0:
        return html.H3('Not logged in.')
    # Get authorization code Google sent back to you
    code = request.args.get("code")

    # Find out what URL to hit to get tokens that allow you to ask for
    # things on behalf of a user
    google_provider_cfg = requests.get(GOOGLE_DISCOVERY_URL).json()
    token_endpoint = google_provider_cfg["token_endpoint"]

    # Prepare and send a request to get tokens
    token_url, headers, body = client.prepare_token_request(
        token_endpoint,
        authorization_response=request.url,
        redirect_url=request.base_url,
        code=code
    )
    token_response = requests.post(
        token_url,
        headers=headers,
        data=body,
        auth=(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET),
    )

    client.parse_request_body_response(json.dumps(token_response.json()))
    userinfo_endpoint = google_provider_cfg["userinfo_endpoint"]
    uri, headers, body = client.add_token(userinfo_endpoint)
    userinfo_response = requests.get(uri, headers=headers, data=body)

    if userinfo_response.json().get("email_verified"):
        unique_id = userinfo_response.json()["sub"]
        user_email = userinfo_response.json()["email"]
        user_name = userinfo_response.json()["given_name"]
        return html.H3('User verified via Google.')
    else:
        return html.H3('User email not available or not verified by Google.')

if __name__ == "__main__":
    app.run_server(port=8800, debug=True)
1 Answers

Having faced a similar situation, I ended up using a modified version of Dash Google OAuth.

While the library allows the user's name to be accessed via flask.request.cookies.get('AUTH-USER'), given the requirement of getting the user's email ID (verified by Google), you could make the following change(s) to the library's google_auth.py file:

...

COOKIE_AUTH_USER_EMAIL = 'AUTH-USER-EMAIL'

...

class GoogleAuth(Auth):
    ...
    def login_callback(self):
        ...
        google = self.__get_google_auth(token=token)
        resp = google.get(os.environ.get('GOOGLE_AUTH_USER_INFO_URL'))
            if resp.status_code == 200:
                user_data = resp.json()
                r = flask.redirect(flask.session['REDIRECT_URL'])
                r.set_cookie(COOKIE_AUTH_USER_NAME, user_data['name'], max_age=COOKIE_EXPIRY)
                # Store (Google) verified user's email ID in a cookie
                r.set_cookie(COOKIE_AUTH_USER_EMAIL, user_data['email'], max_age=COOKIE_EXPIRY)
                r.set_cookie(COOKIE_AUTH_ACCESS_TOKEN, token['access_token'], max_age=COOKIE_EXPIRY)
                flask.session[user_data['name']] = token['access_token']
                return r
        ...
    ...

    @staticmethod
    def logout():
        r = flask.redirect('/')
        r.delete_cookie(COOKIE_AUTH_USER_NAME)
        # Do not forget to delete your custom cookie
        r.delete_cookie(COOKIE_AUTH_USER_EMAIL)
        r.delete_cookie(COOKIE_AUTH_ACCESS_TOKEN)
        return r

... and then access the (Google) verified user's email ID via flask.request.cookies.get('AUTH-USER-EMAIL') (or whichever value you set for COOKIE_AUTH_USER_EMAIL).

Alternatively, if you do not wish to store the user's details (name, email, etc.) in a cookie, you may/could make changes in the above section itself to check the user's email ID against your database before allowing the user access rather than doing so in your Dash app's code.

Related