When faced with the task of having to implement a login mechanism for a web application, I often search for a solution that will cause least of amount of busy work. Admittedly, I don’t believe this is one of the most exciting part of any application. A login mechanism needs to be safe and solid, but it really doesn’t have to be fancy in any way. In most cases a simple solution will do.

The Google Login Button

The Google login button is one the many choices that can simplify this task. In addition to making it easier to set up a login mechanism, it also has the advantage of removing the need to store passwords ourselves, which will enhance security. Validating and storing passwords is not simple. One has to use a secure password hashing algorithm, the passwords have to be compared and checked in a way that prevents side channel attacks (timing attacks), and they have to be stored as securely as possible. By using a token based authentication mechanism where somebody else, such as a big tech company, takes over the responsibility of checking passwords, we reduce the number of security related mistakes we can make. Because the average programmer doesn’t deal often with this kind of responsibility, it makes sense to outsource password checking to the pros.

In order to make this work first we we will need to do the following:

Login Panel Component with the Sign In Button

I created the following component to implement Google login in a way that is simple enough and that works pretty well in react apps. I based it on react-google-login, which makes it feel natural to call the Google API from a react app. You just need to drop in the sign in button tag and give it an onsuccess callback function to customize the behavior and you implemented a Google signin, with minimal work. The same can be done for the Google logout button.

This React Google login panel will show and hide the Google login button and logout buttons based on wether or not the user is logged in via Google. Note, this only gets an ID token from Google and this by itself is not a full login mechanism. This only tackles the first step in the process of authenticating the user. We will also need to get the token out of the object returned by the Google API in the code below, and need to validate it from the server with Google that the token was not tampered with.

import React from 'react';
import {GoogleLogin, GoogleLogout} from 'react-google-login';
import { withRouter } from "react-router-dom";

const GooglePanelInner = props => {

    const responseGoogle = (googleUser) => {
        console.log(JSON.stringify(googleUser));
        const idToken = googleUser.getAuthResponse().id_token;
        const googleEmail = googleUser.profileObj.email;
        console.log('The id_token is ' + idToken)
        localStorage.setItem('idToken', idToken);
        localStorage.setItem('googleEmail', googleEmail);
        props.history.push(props.onLogin);
    };

    const responseGoogleLogout = (response) => {
        console.log('Logout response:' + JSON.stringify(response));
        localStorage.removeItem('idToken');
        localStorage.removeItem('googleEmail');
        console.log('Logged out')
        props.history.push(props.onLogout)
    };

    if (localStorage.getItem('googleEmail') == null)
        return (<div>
            <GoogleLogin
                clientId=<The client ID you created in the credentials panel>
                buttonText="Login with Google"
                onSuccess={responseGoogle}
                onFailure={responseGoogle}
                cookiePolicy={'single_host_origin'}
            />
        </div>)
    else {
        console.log('Logged in to Google with' + localStorage.getItem('googleEmail'));
        return (<div>
            <GoogleLogout
                clientId=<The client ID you created in the credentials panel>
                buttonText="Logout with Google"
                onLogoutSuccess={responseGoogleLogout}
            />
        </div>)
    }
}

export const GooglePanel = withRouter(GooglePanelInner)


googleUser.Zi.id no longer works

Depending on where you get your documentation for how to read the returned ID token, you may think you need to use googleUser.Zi.id_token to read the ID token from the returned response. That stopped working. You need to do it as shown above using googleUser.getAuthResponse().id_token. This is the documentation where I got my info how to read the token: https://developers.google.com/identity/sign-in/web/backend-auth

Properties for automatic redirect

The code above takes two properties, onLogin and onLogout. It uses React Router to redirect the user to the routes passed in, through those properties. Then it can be used like this:

 <GooglePanel onLogin='/signup' onLogout='/'/>

How it works

When the user clicks on the button, a Google login prompt appears and Google is contacted with a request for an ID token. Along with this token, we also get some additional credentials, such an email address and other profile info. You can put in a console.log() statement into the onsuccess callback, so that you can inspect the token that you received in the google developer console. This will make it easier to understand the flow.

It’s important to keep in mind that this information can be very easily manipulated in the browser’s JavaScript run time. Therefore we should never trust it as is. The recommended workflow is to send this ID token to the browser, have the server validate it with Google, and then have the server write an encrypted secure cookie into the validated credentials. At this point we’re safe.

Validating with Google from the server

Note, that the following will work from Java equally well since I’m actually using a Java API from Scala, and it would be very similar from any other server stack. In my particular example I am using Akka HTTP to implement the server side logic. You need to use the appropriate Google API Client for your programming language.

If you choose to use Scala, add this to SBT: “com.google.api-client” % “google-api-client” % “1.30.5”, then the following code can be used to do the validation:

case class VerifiedGoogleIdToken(
userId: String, email: String, emailVerified: Boolean = false,
name: Option[String] = None, pictureUrl: Option[String] = None,
locale: Option[String] = None, 
familyName: Option[String] = None, givenName: Option[String] = None)

def verifyWithGoogle(idTokenString: String): Option[VerifiedGoogleIdToken] = {
      for (
        idToken <- Option(verifier.verify(idTokenString));
        payload = idToken.getPayload();
        verified = VerifiedGoogleIdToken(payload.getSubject(),
          payload.getEmail(),
          payload.getEmailVerified(),
          Option(payload.get("name")).map(_.toString),
          Option(payload.get("picture")).map(_.toString),
          Option(payload.get("locale")).map(_.toString),
          Option(payload.get("family_name")).map(_.toString),
          Option(payload.get("given_name")).map(_.toString)
        );
        _ = l.info(s"Verified Google ID token as $verified")
      ) yield verified
  }

This code just takes the ID token from the browser and sends it to Google, who then verifies that it is correct. If it is, we can get the user’s profile information from the returned response. In the code above, verifier.verify() can return null, so we wrap it immediately in an Option, which is a standard way to handle Java code that can return null in Scala. (In Scala we don’t like nulls!) The returned response has a getPayload() method, which will return a Payload object with all the info we are looking for. We collect all of this into a case class, which is a standard Scala way to pass data around. Case classes are a very safe way to share data, because they are immutable. Then the rest of the application can just use the case class when it wants to access this data.

Unlike in the client, at this point we can trust the information to be correct, because here the returned data cannot be tampered with. Now, we just have to find a way to keep this information around for the duration of the user’s session, so that for each subsequent request we can be certain that the user has been authenticated. This is a perfect task for cookies!

Write the cookies to ‘log the user in’

Now we know that the ID token is good and we need to save it in the user’s browser. So every time the user comes back to us, we would know that we have already verified their identities. The best way to achieve this from Scala is using the Akka HTTP setCoookie function:

val cookie = HttpCookie(
  name, 
  value = <serialized encrypted credentials>, 
  domain = Some(domain), 
  path = Some("/"), 
  secure = true, 
  extension = Some("SameSite=None"))
setCookie(cookie)

CORS

Google will not be serving cross site cookies in future versions of Chrome. If you want to allow cross site cookies you have to have SameSite set to None and secure set to true.

Delete the cookies to ‘log the user out’

In order to log the user out, we need to remove our cookies from their browser. Akka HTTP has a deleteCookie, but it won’t do what you think it will do. Deleting a cookie just sets its value to delete. In order to really delete the cookie, we need to write it with an expiration date in the past. Expired cookies will be deleted from the browser. So you could just issue the same set cookie with the expired option set:

HttpCookie(name, value = , domain = Some(domain), path = Some("/"), secure = true, extension = Some("SameSite=None"), expires = Some(DateTime.now.-(10000)))

Conclusion

Using the Google Login Button is a simple way to authenticate without having to store passwords yourself. It removes the responsibility from the application programmer to process and securely store passwords. Considering that this is no trivial task, it makes sense to delegate this responsibility to the Google login API.