Google One-Tap Sign In using React and Falcon

I'm working on a passion project in my spare time and came across an interesting substitute for the traditional Google oAuth flow: Google's One-Tap Sign In. Humorously, the library's API functions refer to at as googleyolo, which sounds even better.

The library is great, but it's definitely still a little sparse in terms of examples, NPM support etc.

I decided to give it a go with my current tech stack:

  • Frontend: React (react-create-app, ejected)
  • Backend: Falcon

After playing around for a bit, I got it working nicely with the following pattern:

index.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="utf-8">
    <meta name="google-signin-client_id" content="561841481443-k841boidbrl0nrdcqeho6dk8kuq2q296.apps.googleusercontent.com">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>flair</title>
</head>  
<body>  
<div id="root" style="height: 100%;"></div>  
</body>  
<script src="https://smartlock.google.com/client"></script>  
<script src="https://apis.google.com/js/platform.js?onload=authCallback" async defer></script>  
</html>  

index.js:

import React, {Component} from 'react';  
import {authenticate} from '../../data/API'

class Splash extends Component {

    constructor(props) {
        super(props);
        window.authCallback = this.authCallback.bind(this);
    }

    /**
     * On load, invoke the callback from the platform.js script tag
     */
    authCallback() {
        this.renderLoginButton().then(() => {
            this.props.router.push('/home')
        }).catch(() => {
            this.initYolo();
        });
    };

    /**
     * First, attempt to render the Google Sign In button
     * On success -> authenticate using the User ID token
     * On failure -> initialise the Google 'Yolo' Sign In/Register prompt
     * @returns {Promise}
     */
    renderLoginButton() {
        return new Promise((resolve, reject) => {
            window.gapi.signin2.render('my-signin2', {
                scope: 'profile email',
                width: 240,
                height: 50,
                longtitle: true,
                theme: 'dark',
                onsuccess: (user) => {
                    this.onSuccess(user, resolve);
                },
                onfailure: (reason) => {
                    this.onFailure(reason, reject);
                },
            });
        });
    };

    /**
     * Display the Google One Tap Sign in prompt
     */
    initYolo() {
        const hintPromise = window.googleyolo.hint({
            supportedAuthMethods: [
                "https://accounts.google.com"
            ],
            supportedIdTokenProviders: [
                {
                    uri: "https://accounts.google.com",
                    clientId: "561841481443-k841boidbrl0nrdcqeho6dk8kuq2q296.apps.googleusercontent.com"
                }
            ]
        });
        hintPromise.then((credential) => {
            if (credential.idToken) {
                console.log(credential.idToken)
                authenticate(credential.idToken)
                // Send the token to your auth backend.
                //useGoogleIdTokenForAuth(credential.idToken);
            }
        }, (error) => {
            switch (error.type) {
                case "userCanceled":
                    // The user closed the hint selector. Depending on the desired UX,
                    // request manual sign up or do nothing.
                    break;
                case "noCredentialsAvailable":
                    // No hint available for the session. Depending on the desired UX,
                    // request manual sign up or do nothing.
                    break;
                case "requestFailed":
                    // The request failed, most likely because of a timeout.
                    // You can retry another time if necessary.
                    break;
                case "operationCanceled":
                    // The operation was programmatically canceled, do nothing.
                    break;
                case "illegalConcurrentRequest":
                    // Another operation is pending, this one was aborted.
                    break;
                case "initializationError":
                    // Failed to initialize. Refer to error.message for debugging.
                    break;
                case "configurationError":
                    // Configuration error. Refer to error.message for debugging.
                    break;
                default:
                // Unknown error, do nothing.
            }
        });
    }

    onSuccess = (googleUser, resolve) => {
        const id_token = googleUser.getAuthResponse().id_token;
        authenticate(id_token);
        resolve();
    };

    onFailure = (reason, reject) => {
        console.log(reason);
        reject(reason);
    };

    render() {
        return (
            <div className="body">
                <div id="my-signin2"/>
            </div>
        )
    }
}

export default Splash;  

API.js:

import axios from 'axios';

let request = axios.create({  
    withCredentials: true
});

const authenticate = (token) => {  
    request.post('https://localhost:8000/auth', {
        token: token
    }).then((response) => {
        console.log(response);
    })
};

And finally, the server-side code (auth.py):

from google.oauth2 import id_token  
from google.auth.transport import requests

CLIENT_ID = '<YOUR_CLIENT_ID>'

class AuthResource:  
    def on_post(self, req, resp):
        token = req.media['token']
        try:
            idinfo = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)

            if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
                raise ValueError('Wrong issuer.')

            # ID token is valid. Get the user's Google Account ID from the decoded token.
            userid = idinfo['sub']
            resp.set_cookie('flair-token', token, max_age=900)
            resp.media = {'flair-js-token': token}
        except ValueError:
            # Invalid token
            pass


def init(api):  
    api.add_route('/auth', AuthResource())

The One Tap Sign In works fine on it's own, but it just made sense to use it in conjunction with the Google Sign In button, since it checks for valid cookies (and provides a call-to-action) before the One-Tap Hint dialog is displayed.

That's it.