DEV Community

Cover image for Passwordless sign-in with Google One Tap for Web
Danny Perez
Danny Perez

Posted on • Originally published at intricatecloud.io

Passwordless sign-in with Google One Tap for Web

I sign in with my Google account everywhere I can to avoid having yet-another-password on another random website. I've been seeing an upgraded experience on some sites (maybe I'm just noticing now) like Trello/Medium where you can sign-in with Google with one click on the page without getting redirected. Turns out its called One Tap for Web and its Google's passwordless sign-in option and you can use it on your own website. I took it for a spin and set it up on a hello world React example and here's what I found, warts and all.

If you'd like to watch a video version of this tutorial, you can check it out on my YouTube channel here.

Create a new react app and add a sign-in state

Start with a bare react app... npx create-react-app one-tap-demo

For this example app, I'll be using the JS library to initialize the one-tap prompt. The reference guide shows you how to add it using mostly HTML, but if you're using a front-end framework, its easier to use just JS to configure it.

Add some state to keep track of when the user has signed in

function App() {
+   const [isSignedIn, setIsSignedIn] = useState(false)
    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo" />
+                { isSignedIn ? <div>You are signed in</div> : <div>You are not signed in</div>}
            </header>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Add the GSI

Add the Google Sign-In library (also called GSI) dynamically when your application starts

function App() {
    const [isSignedIn, setIsSignedIn] = useState(false)

+     const initializeGsi = () => {
+        google.accounts.id.initialize({
+            client_id: 'insert-your-client-id-here',
+        });
+        google.accounts.id.prompt(notification => {
+            console.log(notification)
+        });
+ }
+
+    useEffect(() => {
+        const script = document.createElement('script')
+        script.src = 'https://accounts.google.com/gsi/client'
+        script.onload = initializeGSI()
+        script.async = true;
+        document.querySelector('body').appendChild(script)
+    }, [])

}
Enter fullscreen mode Exit fullscreen mode

There's 2 API calls - one to configure the library, and one to show the user the prompt (after the library has been configured). To see all possible configuration options, check the reference here.

I added it as a useEffect hook with [] as an arg, so that it only runs once after the first render. ref

When you refresh the page, if you got it all right the first time, you'll see the prompt.

Get a Client ID from your Google Developer Console

Follow this guide for adding your Get your Google API client ID

When I refreshed the page after following the steps and adding my client id to the app, I got this error message:

[GSI] Origin is not an authorized javascript origin
Enter fullscreen mode Exit fullscreen mode

For me, it meant that my Google OAuth Client ID is misconfigured. I missed the "Key Point" on the official walkthrough - which only applies if we're using localhost as our domain.

  • Confirm that your site URL (i.e. http://localhost:3000) is added as both an authorized Javascript origin and as a valid redirect URI, in the Google OAuth Client Console.
  • IMPORTANT You ALSO need to add http://localhost as an authorized Javascript origin. This only seems necessary in development when you might be using a different port in your URL (we are).

Using the sign-in button

Now when you refresh the page, it should be working, and you should see the prompt. If you don't see the prompt, check your developer console for errors, and if that doesn't help - see the section on Debugging below.

IF THIS IS YOUR FIRST TIME DOING THIS, DO NOT CLICK THE X BUTTON ON THE PROMPT BEFORE READING THIS WARNING!

WARNING 1: Clicking the X button on the one-tap prompt dismisses the prompt. If you refresh the page after this, you won't see the button come back. Why?

The One Tap library has some additional side effects around dismissing the prompt. If you've clicked the X button, a cookie has been added to your domain called g_state. Here's a screenshot of where you can find it - if you clear that cookie value, you'll see the prompt come back.

WARNING 2: Clicking the X button more than once will enter you into an Exponential Cool Down mode - see the reference here. What does that mean?

  • You cannot clear cookies or use an incognito window to get around it (at least I couldn't). It seems to be based on your browser and website (maybe IP?), its not clear. If you accidentally run into this, time to take a break. Or try a different browser/website URL.
  • After I dismissed it, I couldn't see it for 10-15 minutes, although the table on the developer guide suggests I wouldn't see it for 2 hours. In any case, its an annoying thing to have to run into during development.

Debugging issues with One Tap sign-in

The developer guide suggests this as example code for your prompt. But it flys over an important detail that the reason WHY your prompt did not display, or got skipped, or got dismissed is also in the notification object.

google.accounts.id.prompt(notification => {
      if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
                  // continue with another identity provider.
      }
})
Enter fullscreen mode Exit fullscreen mode

There are 3 types of notification moments - display, skipped, dismissed and each one has their own list of possible reasons, with its own API call to figure it out - see here for the full list. If you're having problems with the button and you don't know why, it might be helpful to use the snippet below to see what those reasons look like:

google.accounts.id.prompt(notification => {
+      if (notification.isNotDisplayed()) {
+          console.log(notification.getNotDisplayedReason())
+      } else if (notification.isSkippedMoment()) {
+          console.log(notification.getSkippedReason())
+      } else if(notification.isDismissedMoment()) {
+          console.log(notification.getDismissedReason())
+      }
        // continue with another identity provider.
    });
Enter fullscreen mode Exit fullscreen mode

One of the reasons you might see is opt_out_or_no_session. This can mean that

  • A. your user has "opted out" of the prompt by dismissing it. You can try clearing the g_state cookie that might be on your domain, if you dismissed it by accident
  • B. your user does not have a current Google session in your current browser session.
    • While this is a passwordless sign-on via Google, it does require you to have signed into Google (presumably with a password) at some earlier point.
    • If you are using an Incognito window, make sure you sign in to Google within that window.

Now that your user has signed in

Once you have selected your account and signed in with no errors, its time to hook it into your React app. If you've used the Google Sign-In for Websites library before (see here for my guide on setting it up), there's an API to let you get at the user's info. But with the One Tap Sign-In for Web library, you only get the users id token (aka their JWT token).

That means you need to decode the ID token to get the users info. We can do that by adding the jwt-decode library with npm install --save jwt-decode

To do all this, add a callback to your initialize block:

+ import jwt_decode from 'jwt-decode'

function App() {
    const [isSignedIn, setIsSignedIn] = useState(false)
+   const [userInfo, setUserInfo] = useState(null)
+   const onOneTapSignedIn(response => {
+       setIsSignedIn(true)
+       const decodedToken = jwt_decode(response.credential)
+       setUserInfo({...decodedToken})
+   })

     const initializeGsi = () => {
        google.accounts.id.initialize({
            client_id: 'insert-your-client-id-here',
+            callback: onOneTapSignedIn
        });
        ...
 }
    ...
    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo" />
+               { isSignedIn ?
+                   <div>Hello {userInfo.name} ({userInfo.email})</div> :
+                   <div>You are not signed in</div>
+               }
            </header>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

To see all the user info that is available to you, see the dev guide here

Signing out

The docs suggest adding a <div id="g_id_signout"></div> to your page, but its not clear if the library is supposed to create a sign-out button for you. I think the answer is no, because I tried it, and nothing happens.

My current theory is that signing-out is meant to be left to your own application, and can be as easy as refreshing the page.

  • In this post, I'm only using the One-Tap button on the front-end since I have no login system myself. Whenever you refresh the page, you'll see the prompt even if you just finished signing in.
  • If I wanted to integrate this with an existing login system, "sign-out" would mean signing out of my own application (and not out of my google account)
  • This flow works out as long as you dont enable the auto sign-in option.
+ const signout = () => {
+     // refresh the page
+     window.location.reload();
+ }

return (
    <div className="App">
        <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            { isSignedIn ?
+               <div>
+                   Hello {userInfo.name} ({userInfo.email})
+                   <div className="g_id_signout"
+                       onClick={() => signout()}>
+                       Sign Out
+                    </div>
               </div> :
               <div>You are not signed in</div>
            }
        </header>
    </div>
)

Enter fullscreen mode Exit fullscreen mode

Limitations of the One Tap Sign-In button

Also lost in the developer guide is this comment for the prompt snippet // continue with another identity provider. which brings us to the section on limitations of this sign-in prompt.

  • The One Tap Sign-In button only works on Chrome & Firefox across Android, iOS, macOS, Linux, Window 10. If you have users on Safari or Edge, they won't see the prompt. When I try it, I get a Not Displayed error with reason opt_out_or_no_session 🤷
  • If your users accidentally dismiss the prompt (see the warning above if you missed it the first time), they will also see opt_out_or_no_session as the Not Displayed reason, and they will be unable to sign-in.
  • The library (and the interface itself) is different from the Google Sign-In for Web library. The One Tap library uses google.accounts.id.initialize() to initialize the app, and the other one uses gapi.auth2.init() - That seems like a missed opportunity to put both login systems behind the same interface.
  • There's no sign out button - the snippet thats mentioned in the docs doesn't seem to do anything. My guess is that a sign-out button could mean refreshing the page which would cause the prompt to appear again, effectively signing you out.

It's highlighted on the main page of the dev docs, but you can't use this library alone. I did it here for the purposes of a hello world example, but its meant to be an upgrade to your login experience.

Try it out

I've pushed this sample code to github available on intricatecloud/google-one-tap-web-demo. Instructions to run the demo in the README.

It's worth taking a look at how some of these other sites have implemented the login workflow. Sign in to Google in your current browser session, and then visit medium.com to see the prompt in action.

Discussion (0)