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
/login
endpoint. - 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.edu
GOogle 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:1930
as 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
script
tag:<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.js
script we used in lecture 16, and import it in your JavaScript. - When the page loads (e.g. in your
App
class), construct a newGoogleAuth
instance and callrender
to show the button. You can customize the look of the button if you want. - The callback you pass to
render
will be called with anidToken
argument. 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; }
apiRequest
from assignment 3.1.) - In future API requests, include an
Authorization
header with the value`Bearer ${API_KEY}`
. (You may want to modifyapiRequest
to 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_ID
constant with the same client ID as you used on the frontend, as well as aJWT_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, 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 }); });
data
object has a number of fields; you canconsole.log
it to see what's in it. You can also adjust the expiration time on thejwt.sign
line 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
/protected
will haveres.locals.user
defined, and will return a 403 if noAuthorization
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!