Sign in with Google
I got tired of writing the same OAuth code for every little app, so Berth does it once. It puts Google sign-in in front of any app and tells the app who's signed in with request headers.
Set it up once
berth auth:setup google
Google doesn't let programs create OAuth clients, so this walks you through
about a minute in the Google Cloud console: create a "Web application" client
with the redirect URI it prints (https://auth.<your domain>/callback),
publish the consent screen, and paste in the client ID and secret. Every app
shares that one client. Then open https://auth.<your domain> and sign in to
check it works.
If auth.<your domain> is already taken, pick another name:
berth auth:setup google --host signin.<your domain>. Berth won't take over a
name that already has its own DNS record.
Choose who gets in
berth access myapp public # anyone; the app still learns who's signed in
berth access myapp signed-in # any Google account
berth access myapp allow [email protected] @family @example.com
berth access myapp /admin/* allow [email protected] # a rule for some paths (* is a prefix)
berth access myapp /admin/* --remove
berth access myapp # show the policy
berth access myapp off # no sign-in at all
allow takes email addresses, Google Workspace domains (@example.com) and
groups. It won't take @gmail.com: Google doesn't vouch for a domain on
personal accounts, so it would match nobody. Use signed-in for "any Google
account". Make a group once and use it anywhere:
berth auth:group family [email protected] [email protected]
Changes to allow lists and groups apply on the visitor's next request. Anyone not signed in gets sent to Google first (API calls get a 401 instead), and anyone signed in without access sees a "No access" page with a button to switch accounts.
In your app
Requests arrive with these headers. Berth strips any X-Berth-* headers a
visitor sends, so you can trust them.
| Header | Value |
|---|---|
X-Berth-User-Id |
google:<id>, stable for the life of the account: key your users table on this |
X-Berth-Email |
[email protected] |
X-Berth-Name |
display name, URL-encoded (decodeURIComponent it) |
X-Berth-Picture |
avatar URL |
X-Berth-Identity |
all of the above as a 5-minute signed JWT (EdDSA), if you'd like to verify; the key is at /_berth/jwks.json |
On a public app the headers are missing for anonymous visitors. That's the
pattern for a site anyone can read but only signed-in people can post to:
check for the header on your write endpoints, and return 401 if it's not
there.
Apps with a policy also get these paths. On an app with no policy they answer
404, with a note saying berth access myapp public turns them on (and keeps
the app open to everyone):
| Path | What |
|---|---|
/_berth/login?return=/somewhere |
sign in, then come back |
/_berth/logout?return=/path |
sign out of every app (POST it from a form; a GET shows a button), then go back to the path, or to a "Signed out" page if that path needs sign-in |
/_berth/me |
who's signed in, as JSON, for single-page apps |
Once you've signed in to one app, the next is a quick redirect with no Google
prompt. Sessions last 30 days and renew as long as they're used.
berth auth:sessions lists them, berth auth:revoke <email> signs someone out
everywhere, and berth auth:status shows the setup, groups and every app's
policy. Chirp is a complete example.