christianespejo.comSantiago, Chile

Google Login and Service Accounts for your App

This tutorial describes how to add Google sign-in to a website or web application, restrict access to a defined list of accounts, and create a Google service account that allows server-side code to read and write a Google Sheet. The Google Cloud configuration is independent of the web framework in use. The procedure is demonstrated on a Next.js application.

1. What is being built, and what is required

The finished application has three properties. Visitors can sign in with a Google account. Only a defined list of accounts is admitted. Server-side code can read and write a Google Sheet using a service account.

The following are required:

  • A Google account with access to the Google Cloud console.
  • A web application with a server component. The examples use Next.js. The Google Cloud steps do not depend on the framework.
  • A Google Sheet to serve as the data store.
  • A location for environment variables: a .env.local file during development and the hosting provider's settings in production.
  • A Git repository in which .env.local is listed in .gitignore before any credential is created.

2. Create the Google Cloud project

A Google Cloud project holds the OAuth client, the service account, and the enabled APIs for one application.

  1. Open the Google Cloud console and select the project picker at the top of the page.
  2. Select New project.
  3. Enter a project name. Google generates a project ID from it. The ID cannot be changed afterward.
  4. Select Create, then confirm that the new project is selected in the project picker.

Using a separate project for each application keeps credentials and permissions isolated from one another.

3. Configure the consent screen and create an OAuth client

The OAuth client identifies the application to Google. The consent screen must be configured before a client can be created.

  1. In the Google Cloud console, open Google Auth Platform. If the page reports that it is not configured, select Get started.
  2. Under App information, enter an application name and a support email address.
  3. Under Audience, select a user type:
    • Internal: only accounts in the same Google Workspace organization can sign in.
    • External: any Google account can sign in. While the application is in testing status, only accounts listed as test users are admitted.
  4. Enter a contact email address, accept the user data policy, and select Create.
  5. Open Clients and select Create client.
  6. Set the application type to Web application and enter a name. The name is a label for the console only.
  7. Under Authorized JavaScript origins, add each origin from which the sign-in button is served, for example http://localhost:3000 and the production domain. Origins must use HTTPS, except for localhost, and must not contain a path or a wildcard. A mismatch produces an origin_mismatch error.
  8. Under Authorized redirect URIs, add the exact URL of the server endpoint that will receive the sign-in response, for example http://localhost:3000/api/auth/google and its production equivalent. The value must match exactly. A mismatch produces a redirect_uri_mismatch error, and changes can take from five minutes to several hours to take effect.
  9. Select Create and copy the client ID.

The client ID is not a secret. It appears in the page that displays the sign-in button.

4. Add Google login to the application

The sign-in flow has two parts: a page that displays Google's button, and a server endpoint that receives and verifies the response.

The page. Load Google's script and render the button in redirect mode. data-login_uri must exactly match the redirect URI registered in section 3.

HTML
<script src="https://accounts.google.com/gsi/client" async></script>
<div id="g_id_onload"
     data-client_id="YOUR_CLIENT_ID.apps.googleusercontent.com"
     data-ux_mode="redirect"
     data-login_uri="http://localhost:3000/api/auth/google"
     data-auto_prompt="false"></div>
<div class="g_id_signin" data-type="standard"></div>

In Next.js, load the script with the next/script component, and write class as className in JSX.

The endpoint. Google sends the result to data-login_uri as a POST request containing a credential field (the ID token) and a g_csrf_token field. The same token is also set as a cookie. The endpoint compares the two values, then verifies the ID token with Google's client library, installed with npm install google-auth-library.

The endpoint reads the client ID from an environment variable. Add the client ID copied in section 3 to .env.local:

Plain text
GOOGLE_CLIENT_ID=YOUR_CLIENT_ID.apps.googleusercontent.com
TypeScript
// app/api/auth/google/route.ts
import { NextRequest } from 'next/server'
import { OAuth2Client } from 'google-auth-library'

const client = new OAuth2Client()

export async function POST(request: NextRequest) {
  const form = await request.formData()
  const credential = form.get('credential')
  const csrfBody = form.get('g_csrf_token')
  const csrfCookie = request.cookies.get('g_csrf_token')?.value

  if (!credential || !csrfBody || csrfBody !== csrfCookie) {
    return new Response('Bad request', { status: 400 })
  }

  try {
    const ticket = await client.verifyIdToken({
      idToken: String(credential),
      audience: process.env.GOOGLE_CLIENT_ID,
    })
    const payload = ticket.getPayload()
    // Verified. `payload` holds the claims. The authorization check follows in section 5.
    return Response.redirect(new URL('/', request.url), 303)
  } catch {
    return new Response('Unauthorized', { status: 401 })
  }
}

verifyIdToken checks the token's signature, issuer, expiry, and audience. The sub field of the payload is the stable identifier of the Google account. The email address can change and should not be used as the identifier.

On success the endpoint must return a response. The example redirects to the home page with status 303, which makes the browser follow the POST with a GET.

After this step the user is authenticated. Creating a session from the verified payload depends on the framework and is outside the scope of this tutorial.

5. Restrict sign-in to authorized accounts

A successful Google sign-in proves who the user is. It does not establish that the user is permitted to use the application, because any Google account can complete it. The permission check must be made on the server, after the token is verified.

Store the permitted addresses in an environment variable, comma separated:

Plain text
ALLOWED_EMAILS=user1@example.com,user2@example.com

Then replace the comment in the endpoint from section 4 with this check:

TypeScript
const allowed = (process.env.ALLOWED_EMAILS ?? '')
  .split(',')
  .map((e) => e.trim().toLowerCase())
  .filter(Boolean)

const email = payload?.email?.toLowerCase()
if (!email || !payload?.email_verified || !allowed.includes(email)) {
  return new Response('Forbidden', { status: 403 })
}

The check requires email_verified to be true, so an unverified address cannot be used to impersonate a permitted one. To admit every account of one Google Workspace organization, compare payload.hd with the organization's domain instead.

An address list is practical for a small number of users. For a larger user base, record each user's sub identifier on first sign-in and match on that.

6. Create the service account and key

A service account is an identity for server-side code. It authenticates with a private key rather than a user's sign-in, and it is independent of the login flow in sections 3 to 5.

  1. Open APIs & Services, then Library, search for Google Sheets API, and select Enable.
  2. Open IAM & Admin, then Service accounts, and select Create service account.
  3. Enter a name and a description, then select Create and continue.
  4. Skip the role step. Access to the spreadsheet is granted by sharing the sheet in section 7, not by a project role. Select Done.
  5. Select the new service account, open the Keys tab, and select Add key, then Create new key.
  6. Choose JSON and select Create. The key file downloads once. Google does not keep a copy of the private key.

The file grants access as the service account and must be handled as a password. Section 9 describes how to store it.

Some organizations block key creation with the Disable service account key creation policy. If the option is unavailable, an administrator with the Organization Policy Administrator role must change that policy.

7. Share the Google Sheet and call the API from the server

A service account can reach only the spreadsheets that have been shared with it. Sharing is the step most often missed.

  1. Open the downloaded JSON file and copy the value of client_email. It ends in .iam.gserviceaccount.com.
  2. Open the Google Sheet and select Share. Add that address and assign Editor access, or Viewer if the application only reads.
  3. Copy the spreadsheet ID from the sheet's address: https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit.
  4. Store the key and the ID as environment variables:
Plain text
GOOGLE_SERVICE_ACCOUNT_JSON={"type":"service_account", ...}
SPREADSHEET_ID=your-spreadsheet-id

The downloaded file is spread over several lines. Collapse it onto a single line before pasting it, and leave the \n sequences inside private_key as they are. In .env.local, do not wrap the value in double quotes, because they turn \n into real line breaks and JSON.parse then fails.

  1. Install the client library with npm install googleapis, and call the API from server code:
TypeScript
import { google } from 'googleapis'

const auth = new google.auth.GoogleAuth({
  credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON!),
  scopes: ['https://www.googleapis.com/auth/spreadsheets'],
})
const sheets = google.sheets({ version: 'v4', auth })

// Read
const res = await sheets.spreadsheets.values.get({
  spreadsheetId: process.env.SPREADSHEET_ID,
  range: 'Sheet1!A1:C10',
})
console.log(res.data.values)

// Append
await sheets.spreadsheets.values.append({
  spreadsheetId: process.env.SPREADSHEET_ID,
  range: 'Sheet1!A:C',
  valueInputOption: 'USER_ENTERED',
  requestBody: { values: [['Alice', 'checked in', new Date().toISOString()]] },
})

This code must run on the server only, and only for requests that have passed the check in section 5. The key never leaves the server. The read and append calls are shown as bare statements; run them inside an async function, such as a route handler.

8. Test as an outsider

Each control should be tested by attempting to bypass it.

TestExpected result
Sign in with an account on ALLOWED_EMAILSThe endpoint passes the check and redirects to the home page
Sign in with a Google account that is not on the list403 Forbidden
Send a request without the CSRF token: curl -i -X POST http://localhost:3000/api/auth/google -d "credential=test"400 Bad request
Request a spreadsheet that has not been shared with the service accountA 403 permission error from the Sheets API

If Google reports redirect_uri_mismatch or origin_mismatch during sign-in, compare the address in the browser with the values registered in section 3. They must match exactly, including the protocol and port.

9. Keep credentials safe

  • Exclude secrets from Git before creating them. List .env.local and *service-account*.json in .gitignore.
  • Store the key as an environment variable. Place the contents of the JSON file in the hosting provider's environment settings, then delete the downloaded file.
  • Distinguish public values from secrets. The OAuth client ID is public. The service account key is a secret and must never be placed in a variable that the framework exposes to the browser, such as one prefixed with NEXT_PUBLIC_ in Next.js.
  • Check the repository history. Before making a repository public, run git log --all -S"private_key" --oneline. Any result means a key was committed and must be revoked.
  • Grant the minimum access. Share only the required spreadsheets with the service account, and use Viewer access wherever the application does not write.
  • Delete keys that are no longer used. In IAM & Admin, open Service accounts, select the account, open Keys, and delete the key. Rotate the key at once if it may have been exposed.