Documentation Menu

Sign-in with GitHub or Google

7 min read

Let people sign in with their GitHub or Google account, limited to your organisation, and close the password sign-up form to everyone else.

Why your organisation's member list is the door

A fresh instance lets anyone who reaches it create an account. That is how the first owner gets in, and it is also how a stranger would get in. Most teams already keep a list of who belongs, either as the members of a GitHub organisation or as the accounts of a Google Workspace domain, and Quillet uses that list as its door. Content managers sign in with the account they already have, so nobody keeps another password. When someone leaves the organisation, they lose access at their next sign-in, and nobody has to touch the CMS.

What the three sign-in settings change

Three settings work together. All three are environment variables, not a screen in the dashboard:

  • Closed registration: AUTH_REGISTRATION=closed turns the password sign-up form off for everyone. The form disappears from the dashboard and POST /api/v1/auth/register answers 403. Every account that already exists keeps working, its password included.
  • Sign-in with GitHub: an OAuth App's client id and secret, together with a list of allowed organisations, add Continue with GitHub under the sign-in form.
  • Sign-in with Google: an OAuth client from Google Cloud Console, together with a list of allowed Workspace domains, adds Continue with Google.

When someone signs in through a provider, the CMS asks the provider who they are. It asks with a token that belongs to that person and drops the token straight after, so there is no organisation-wide credential to guard. The CMS asks GitHub whether the person is an active member of one of the listed organisations, private memberships included. It asks Google for the account's hosted domain, which only Workspace accounts carry and only Google sets.

The list is the allow-list. A member gets an account on their first sign-in, whether or not registration is closed. Anyone not on the list is refused at that provider's button, even if they already have an account, although an account with a password can still sign in with that password. The check runs on every provider sign-in, not only the first.

The CMS matches accounts in this order:

  1. An account already linked to this GitHub or Google identity signs in as itself, even if its email address has changed since.
  2. Otherwise, an account whose email address matches the provider's verified address is linked and signed in. A password account becomes a provider account this way and keeps its password. One account can carry both providers.
  3. Otherwise, the CMS creates a new account with the person's name and verified address, and no password.

The CMS never uses an address the provider has not verified, so nobody can claim an existing account by putting its address on their own. An account created by a provider has no password, and a password attempt against it gets the same answer as any wrong password.

When the CMS refuses a sign-in, the person lands back on the sign-in page with one sentence that says why. The reason is one of these: the sign-in was cancelled, the link expired, the account has no verified address, the account is not in an allowed organisation or domain, or the provider did not complete the sign-in. The CMS logs each refusal as one warn line that names the provider and the reason, never the token.

Each provider route is rate-limited per address, the same way password sign-in is: AUTH_RATE_LIMIT attempts per AUTH_RATE_LIMIT_WINDOW_MS, counted for each route on its own.

How to

First register the CMS with the provider. Both providers send the browser back to a callback URL built from BASE_URL, so before you start, set BASE_URL to the address people actually use.

GitHub

  1. In GitHub, open Settings, then Developer settings, then OAuth Apps, and choose New OAuth App. Create it under the organisation if you can, so that it does not belong to one person.
  2. Set Homepage URL to your BASE_URL and Authorization callback URL to <BASE_URL>/api/v1/auth/github/callback.
  3. Register the app, generate a client secret, and copy the client id and the secret.
  4. If the organisation restricts third-party application access, which newer organisations do by default, an organisation owner has to approve the app once under the organisation's OAuth application policy. Until then, the CMS refuses every member as not being in an allowed organisation.

The CMS asks for the scopes read:user, user:email and read:org, and nothing more.

Google

  1. In Google Cloud Console, open APIs & Services, then Credentials, and create an OAuth client ID of type Web application.
  2. Add <BASE_URL>/api/v1/auth/google/callback as an Authorised redirect URI. Google accepts http:// there only for localhost, so anywhere else the CMS needs https://.
  3. Copy the client id and the client secret.

The consent screen may be internal or external. The domain check is the CMS's own, so it holds either way. The CMS asks for openid, email and profile.

Then give the CMS the credentials and the list, and close registration.

Docker

Add these to the environment of the CMS service, or to your env file. Keep the secrets in your secret store, not in the file you commit:

AUTH_REGISTRATION=closed
AUTH_GITHUB=on
AUTH_GITHUB_CLIENT_ID=<the OAuth App's client id>
AUTH_GITHUB_CLIENT_SECRET=<the OAuth App's client secret>
AUTH_GITHUB_ALLOWED_ORGS=<your-org>
AUTH_GOOGLE=on
AUTH_GOOGLE_CLIENT_ID=<the OAuth client id>
AUTH_GOOGLE_CLIENT_SECRET=<the OAuth client secret>
AUTH_GOOGLE_ALLOWED_DOMAINS=<example.com>

Leave out the provider you do not use, or set it to off. Then recreate the container so it reads the new values, for example with docker compose up -d --wait cms.

Helm

Set the provider and the list in your values file. Put the client secret either in secret or as a key of your existingSecret (AUTH_GITHUB_CLIENT_SECRET, AUTH_GOOGLE_CLIENT_SECRET):

config:
  authRegistration: closed
auth:
  github:
    enabled: true
    clientId: <the OAuth App's client id>
    allowedOrgs: <your-org>
  google:
    enabled: true
    clientId: <the OAuth client id>
    allowedDomains: <example.com>

Apply it with helm upgrade. A change to config or auth rolls the pods by itself. A new key in an existingSecret does not, so restart the deployment after you add one.

Both lists are comma-separated, and the CMS compares them without regard to case. Under on, a missing client id, secret or list stops the CMS from starting, and its log names each missing value. Check what the sign-in page will offer:

curl -s http://localhost:3000/api/v1/auth/options

With both providers on and registration closed, it prints {"registration":"closed","github":true,"google":true}. Open the sign-in page in a private window, choose Continue with GitHub or Continue with Google, and sign in as a member. You land in the dashboard. A newly created account belongs to no project yet, and an owner adds it in the project's Settings, under Members.

Limits

  • Everything here is configuration. The dashboard has no screen for turning a provider on, editing a list or closing registration, so each change is an environment change and a restart.
  • The allow-lists are GitHub organisations and Google Workspace domains. GitHub teams, Google groups and lists of individual people are not supported.
  • A personal Google account has no hosted domain, so a domain list never admits one. Only * does, and then it admits every Google account.
  • Password sign-in cannot be turned off. Closing registration stops new password accounts, and existing ones keep their passwords.
  • A provider identity cannot be unlinked from an account, and the dashboard does not show which providers an account is linked to.
  • Signing in does not grant access to any project. An owner still adds each person as a member, after their first sign-in has created the account.
  • A GitHub organisation with third-party access restrictions refuses everyone until an owner approves the app.
  • GitHub and Google are the only providers.
  • Configuration reference has every sign-in variable, with what breaks when each one is wrong.
  • Behind a proxy and TLS explains why BASE_URL decides the callback and the cookie.
  • Access explains accounts, tokens and keys, and why a project belongs to a person.
  • Your account shows a content manager how to sign in and manage their own account.
  • Authentication covers the routes behind sign-in and which credentials each API accepts.

Last updated