Google authentication set up
Written by MichaelHere 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:
- 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.
- Once they do, your app will receive an "ID token." You will send this token back to your API backend via a
/loginendpoint. - 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."
- The frontend will include this API key in all future requests to the backend.
- 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:
- You can't use your
@stanford.eduGOogle account for this. Any gmail address will work. You do not need to enter any billing information. Don't activate the "free trial" for GOogle cloud unless you also want to use something else they offer there (since the trial only lasts 90 days). - When creating the application, add
http://localhost:1930as an "Authorized JavaScript origin". The instructions say you should also addhttp://localhost. - You'll be shown a client ID and secret. Save the client ID; you don't need the client secret for our setup.
Frontend
- In your HTML, add this
scripttag:<script src="https://accounts.google.com/gsi/client" defer></script> - Create a
<div>to hold the log in button, and give it an ID (or something you can easilyquerySelector). - Download the
googleauth.jsscript we used in lecture 16, and import it in your JavaScript. - When the page loads (e.g. in your
Appclass), construct a newGoogleAuthinstance and callrenderto show the button. You can customize the look of the button if you want. - The callback you pass to
renderwill be called with anidTokenargument. Make an API request to your backend and store the returned API key, e.g.::
(This uses theasync _onLogin(idToken) { let data = await apiRequest("POST", "/login", { idToken }); window.API_KEY = data.apiKey; }apiRequestfrom assignment 3.1.) - In future API requests, include an
Authorizationheader with the value`Bearer ${API_KEY}`. (You may want to modifyapiRequestto include it, if it is set.
Backend
- Install the necessary packages with
npm install google-auth-library jsonwebtoken. - Then add the relevant imports:
import jwt from "jsonwebtoken"; import { OAuth2Client } from "google-auth-library"; - Define a
CLIENT_IDconstant with the same client ID as you used on the frontend, as well as aJWT_SECRETconstant, 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
nodeREPL. At the terminal, typenode, press enter, and then entercrypto.randomBytes(32).toString("base64"). You'll get a string of characters, which you can use as your secret.
- One quick way to generate a random string is using the
- Now add the login endpoint:
Theapi.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 }); });dataobject has a number of fields; you canconsole.logit to see what's in it. You can also adjust the expiration time on thejwt.signline if you want. - 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(); } }); - Now, every endpoint that starts with
/protectedwill haveres.locals.userdefined, and will return a 403 if noAuthorizationheader 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!