Berth

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.

Edit this page on GitHub (it's a section of the README)