---
title: "Sign in with Google"
audience: "anyone whose visitors should sign in with their Google account"
description: "You'll switch the sign-in to Google, keep working against a stand-in form until the client secret exists and see the redirect the host sends once it does…"
snippet_caption: "config/app.toml"
snippet: "[auth]\nprovider = \"oidc\"\npreset = \"google\"\nclient_id = \"1234.apps.googleusercontent.com\"\nclient_secret = \"google\"\nlogin = \"/sign-in\"\n\n[secrets.google]\ndescription = \"the Google OAuth client secret\""
---

import { Install } from "@src/ui/Install";

You'll switch the sign-in to Google, keep working against a stand-in form until the client secret exists and see the redirect the host sends once it does. No body changes, since a body only reads `identity`.

<Install captured="0.x" />

Start from the app you built in [350](https://snapfirers.com/fsr/learn/350-protect-a-route): the middleware guarding `/account`, the `/account` page with its sign-out form and `config/auth.toml` with `alice`.

## How an OIDC sign-in runs

Google speaks OpenID Connect. The host sends the browser to Google with a `state`, a `nonce` and a PKCE challenge. Google sends it back to `/auth/callback` with a `code`. The host exchanges the code for tokens, checks the ID token's signature against Google's published keys and reads who signed in from its `sub` claim. That becomes `identity.subject`. The other claims of the ID token, such as `email`, arrive in `identity.claims`.

The access token and the refresh token go into custody. A body never sees them.

## Configure the provider

Replace the `[auth]` section:

```toml title="config/app.toml"
[auth]
provider = "oidc"
preset = "google"
client_id = "1234.apps.googleusercontent.com"
client_secret = "google"
login = "/sign-in"

[secrets.google]
description = "the Google OAuth client secret"
```

`preset = "google"` names Google's issuer and its quirks. `microsoft` and `apple` are presets too. Any other issuer takes `issuer` with its base URL instead. `client_id` is the ID from the Google Cloud console.

`client_secret` does not hold the secret. It names a `[secrets]` entry, so the value never sits in `app.toml`. The value comes from the environment variable `C5_SECRETS__GOOGLE__VALUE` or an encrypted overlay.

## Sign in before the secret exists

In development, while `secrets.google` has no value, the method is a stand-in: a "sign in as" form over the users of `config/auth.toml`. Run `fsr dev app` and the boot report says so:

```shell-session
$ fsr dev app
auth      oidc google, a stand-in until secrets.google has a value, login page /sign-in, routes /auth/login, /auth/enrol, /auth/callback and /auth/logout
```

The stand-in presents a form of one field and lists every user of `config/auth.toml`, which here holds `alice` and `bob`. A JSON request shows the step:

```shell-session
$ curl -c jar -b jar -H 'accept: application/json' 'http://127.0.0.1:3000/auth/login?return_to=/account'
{"next":"present","kind":"form","data":{"fields":["user"],"users":["alice","bob"]}}
```

The sign-in page from 350 posted `user` and `password`. The stand-in reads only `user`, so the page needs one field:

```ts title="app/routes/sign-in/page.loader.ts"
import type { Ctx } from "@snapfire/fsr/host";

export async function load({ query }: Ctx<"/sign-in">) {
  return { denied: query.error === "denied" };
}
```

```tsx title="app/routes/sign-in/page.tsx"
import type { SignInProps } from "@generated/client";

export default function SignIn({ denied }: SignInProps) {
  return (
    <section>
      <h1>Sign in</h1>
      {denied ? <p>There is no account by that name.</p> : null}
      <form method="post" action="/auth/callback">
        <label>
          Sign in as <input name="user" autoComplete="username" />
        </label>
        <button type="submit">Sign in</button>
      </form>
    </section>
  );
}
```

Run `fsr dev app` and open `/account`. You land on `/sign-in?return_to=%2Faccount`. Type `alice` and you are on `/account`, signed in as `alice` with her claims. A name that is not in `config/auth.toml` is refused:

```shell-session
$ curl -i -c jar -b jar -d 'user=mallory' http://127.0.0.1:3000/auth/callback
HTTP/1.1 303 See Other
location: /sign-in?error=denied&return_to=%2Faccount
```

Everything behind the sign-in can be written now: the middleware, the loaders reading `identity`, the sign-out form. None of it changes when Google takes over.

## Set the secret

Give the secret a value and start the app:

```shell-session
$ C5_SECRETS__GOOGLE__VALUE=... fsr dev app
```

The stand-in is gone. `/auth/login` now sends the browser to Google:

```shell-session
$ curl -i 'http://127.0.0.1:3000/auth/login?return_to=/account'
HTTP/1.1 303 See Other
location: https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=1234.apps.googleusercontent.com&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fauth%2Fcallback&scope=openid+email+profile&state=xiUno07DO3-BwTIBJ0XGO2VOp9LnSxsG7iApERxlmnI&nonce=paoo6L-zv5dSMuDftHmBX58FX6yiX1ufH1JWahjggY4&code_challenge=HPYLKAWp-gdbfKu-g2GiNHRktIlF9_JVMyyFEL15YeE&code_challenge_method=S256
```

The scopes are `openid email profile` unless `scopes` names others. `state`, `nonce` and `code_challenge` are fresh on every sign-in. Google answers at `/auth/callback` and the visitor lands on `/account`. A refusal comes back to `/sign-in` with `error=denied`, so the page keeps its message. In place of the form it can hold a link to `/auth/login?return_to=/account` labelled "Sign in with Google".

## Register the redirect URI

Google only sends a visitor back to an address registered with the OAuth client. The redirect URI is `<document.origin>/auth/callback`. In development without `document.origin`, the host uses the address it listens on, which is the `http://127.0.0.1:3000/auth/callback` above. Set the origin the site is reached over:

```toml title="config/app.toml"
[document]
origin = "https://shop.example"
```

The redirect now carries `redirect_uri=https%3A%2F%2Fshop.example%2Fauth%2Fcallback`. Register `https://shop.example/auth/callback` in the Google Cloud console. Register the development address too if you want to sign in with Google on your machine.

## Deploy it

Outside development the stand-in is never used. A deployment that has no value for the secret stops at boot. The message names the configuration directory, then the method and the variable to set:

```shell-session
$ RELEASE_ENV=production fsr serve app
.../config: auth.methods `google`: `secrets.google.value` has no value; set C5_SECRETS__GOOGLE__VALUE
```

A deployment with the secret but no `document.origin` stops too, since the host cannot name its redirect URI:

```shell-session
.../config: auth.methods `google`: an oidc method needs `document.origin` to name its redirect URI
```

Next up: [360. Add light and dark themes](https://snapfirers.com/fsr/learn/360-add-light-and-dark-themes).
