Google authentication set up

Written by Michael

Here are some quick instructions to set up "Sign in with Google" for your project.

Overview

Setting up Sign in with Google takes a few steps:

  1. On the frontend, you'll create a "Sign in with GOogle" button. When the user clicks the button, they'll be prompted to log in to their Google account, and then asked to confirm they would like to send their name, email, and profile picture to your app.
  2. Once they do, your app will receive an "ID token." You will send this token back to your API backend via a /login endpoint.
  3. The backend will verify the ID token, which will confirm that it came from Google, and will then have access to the user's name and email address. It will then generate and respond with an "API key."
  4. The frontend will include this API key in all future requests to the backend.
  5. The backend will verify this key whenever it receives it and, through it, will have access to the logged in user.

Getting started

First, follow these instructions to get a client ID. Some notes:

Frontend

  1. In your HTML, add this script tag:
    <script src="https://accounts.google.com/gsi/client" defer></script>
  2. Create a <div> to hold the log in button, and give it an ID (or something you can easily querySelector).
  3. Download the googleauth.js script we used in lecture 16, and import it in your JavaScript.
  4. When the page loads (e.g. in your App class), construct a new GoogleAuth instance and call render to show the button. You can customize the look of the button if you want.
  5. The callback you pass to render will be called with an idToken argument. Make an API request to your backend and store the returned API key, e.g.::
      async _onLogin(idToken) {
        let data = await apiRequest("POST", "/login", { idToken });
        window.API_KEY = data.apiKey;
      }
    (This uses the apiRequest from assignment 3.1.)
  6. In future API requests, include an Authorization header with the value `Bearer ${API_KEY}`. (You may want to modify apiRequest to include it, if it is set.

Backend

  1. Install the necessary packages with npm install google-auth-library jsonwebtoken.
  2. Then add the relevant imports:
    import jwt from "jsonwebtoken";
    import { OAuth2Client } from "google-auth-library";
  3. Define a CLIENT_ID constant with the same client ID as you used on the frontend, as well as a JWT_SECRET constant, which should be a random string. (You need to keep the JWT_SECRET private, because anyone with that string will be able to generate tokens to authenticate to your app.)
    • One quick way to generate a random string is using the node REPL. At the terminal, type node, press enter, and then enter crypto.randomBytes(32).toString("base64"). You'll get a string of characters, which you can use as your secret.
  4. Now add the login endpoint:
    api.post("/login", async (req, res) => {
      let idToken = req.body.idToken;
      let client = new OAuth2Client();
      let data;
      try {
        /* "audience" is the client ID the token was created for. A mismatch would mean the user is
           trying to use an ID token from a different app */
        let login = await client.verifyIdToken({ idToken, audience: CLIENT_ID });
        data = login.getPayload();
      } catch (e) {
        /* Something when wrong when verifying the token. */
        console.error(e);
        res.status(403).json({ error: "Invalid ID token" });
      }
    
      /* data contains information about hte logged in user. */
      let email = data.email;
      let name = data.name;
      //TODO: Do whatever work you'd like here, such as ensuring the user exists in the database
      /* You can include additional information in the key if you want, as well. */
      let apiKey = jwt.sign({ email }, JWT_SECRET, { expiresIn: "1d" });
      res.json({ apiKey });
    });
    The data object has a number of fields; you can console.log it to see what's in it. You can also adjust the expiration time on the jwt.sign line if you want.
  5. Finally, add code to verify the JWT. The easiest way to do this is to put all endpoints that require authentication under a prefix, like /protected, and add a middleware function:
    api.use("/protected", async (req, res, next) => {
      /* Return an authentication error. */
      const error = () => { res.status(403).json({ error: "Access denied" }); };
      let header = req.header("Authorization");
      /* `return error()` is a bit cheesy when error() doesn't return anything, but it works (returns undefined) and is convenient. */
      if (!header) return error();
      let [type, value] = header.split(" ");
      if (type !== "Bearer") return error();
      try {
        let verified = jwt.verify(vakue, SECRET);
        //TODO: verified contains whatever object you signed, e.g. the user's email address.
        //Use this to look up the user and set res.locals accordingly
        next();
      } catch (e) {
        console.error(e);
        error();
      }
    });
  6. Now, every endpoint that starts with /protected will have res.locals.user defined, and will return a 403 if no Authorization header was supplied, or if it's invalid.

We realize these instructions are a bit vague at times, because many details will depend on your own project setup. If you aren't sure how to apply any of these steps, please ask on the forum or come to office hours!