What you’re actually setting up
Two separate things happen when someone signs in, and it’s worth keeping them apart in your head:
| Step | Who does it | Question it answers |
|---|---|---|
| Authentication | Is this really Dana, and does Dana work here? | |
| Authorization | Your app | Now that we know it’s Dana — what is Dana allowed to see? |
Google will never do the second one for you. This guide sets up the first, and the demo app shows the second sitting on top of it in about two lines of code.
The one thing that goes wrong
You are going to choose an audience type called Internal. Internal means internal to the Google organization that owns the project — not “internal” in some general sense.
So the account you use to create the project has to be your own company’s Workspace account. If you sign in with a personal Gmail, or with a consultant’s account at their company, “Internal” will mean their company, and your team will be locked out of your own app.
Check the top-right avatar in the Cloud console before you start. If it doesn’t say your work email, switch accounts first.
Part 1Create the project
-
Go to console.cloud.google.com and sign in with your work account.
-
Click the project dropdown in the top bar → New project.
-
Name it something you’ll recognise later —
sso-demois fine. -
If there’s an Organization or Location field, it should show your company’s domain. If it says No organization, stop and read When “Internal” isn’t available below.
-
Click Create, then make sure the new project is selected in the top bar.
Part 2Set up the sign-in screen
Google renamed this section, so older tutorials will send you to a page that no longer exists. The current name is Google Auth Platform.
-
Type
Google Auth Platforminto the console’s search bar and open it. -
Click Get started.
-
App name — what people will see on the consent screen. Use your company or product name.
User support email — pick your address from the dropdown.
Click Next. -
Audience → choose Internal. Click Next.
- If Internal is greyed out, see below.
-
Contact information — your email again. This is where Google emails you about the project. Click Next.
-
Agree to the policy → Continue → Create.
Confirm the permissions are the boring ones
-
In the left menu, open Data access.
-
Click Add or remove scopes and make sure you have exactly these three, and nothing else:
openid.../auth/userinfo.email.../auth/userinfo.profile
-
Save.
These three tell you a person’s name, email address, and which Workspace they belong to. That’s all. They are the reason this setup needs no review from Google and no approval from your admin — you’re asking to identify people, not to read their mail or their files.
The moment you add a Gmail, Drive, or Calendar scope, you’re in a completely different approval process. Don’t, unless you actually need it.
Part 3Create the credentials
-
In the left menu, open Clients → Create client.
-
Application type → Web application.
-
Name → anything (only you see this).
-
Under Authorized redirect URIs, click Add URI and paste the exact URI your app is showing you.
This is why the app goes up first. Open it now if you haven’t — its waiting-for-credentials screen displays its own redirect URI with a copy button next to it. It looks like one of these:
Example redirect URIshttps://your-app.workers.dev/auth/callback http://localhost:7337/auth/callbackExactly means exactly. Google does a character-for-character comparison.
httpsinstead ofhttp, a wrong port, or a trailing/all produce the same unhelpfulredirect_uri_mismatcherror later. Copy and paste it; don’t retype it.You can register both — a deployed URI and a localhost one — on the same client. That is normal, costs nothing, and lets you debug locally before touching the deployed copy.
-
Click Create.
-
A panel shows your Client ID and Client secret. Copy both now. The secret is only displayed here — if you lose it, you can add a new one from the same page, no harm done.
Done with Google.
Part 4Plug the values in
Back in the app. Nothing here needs a terminal, a redeploy, or a code change — the app has been sitting there waiting for exactly these two values.
-
Open the app’s setup screen at
/setup. If it is showing the waiting-for-credentials page, there is a button straight to it. -
Fill in the form:
Field What goes in it Setup token Not a Google value and not a Cloudflare one. It is a password held by whoever deployed the app, set with wrangler secret put SETUP_TOKEN. If you deployed it yourself, it is the value you chose. Running locally, there is no token — the app only listens on your own machine.Google client ID The long one from step 20, ending in .apps.googleusercontent.comGoogle client secret The shorter one from the same panel Workspace domain The part after the @ in your work email. This is the gate. Admin emails Optional. Put your own address here so you can see the difference between a member and an admin. -
Save and activate. The client secret is written to encrypted storage — not to the repository, not to a config file, and it is never rendered back to a page.
-
Go to the app’s home page and click Sign in with Google.
You should land on a page with your name, your photo, and an admin badge.
If you would rather not have a setup screen at all, the same values can be set as secrets from a terminal and read at startup. The screen exists so that the person who deploys the app and the person who owns the Google Workspace do not have to be the same person — which is usually the real situation.
Part 5Prove it actually works
The demo isn’t interesting until you’ve watched it say no.
Try a personal Gmail. Sign out, sign in again, pick a personal account at the Google prompt. You’ll get Not allowed in, and the page will tell you there’s no hd claim on the account — because personal accounts don’t belong to a Workspace.
Try being a normal employee. Go back to /setup, clear your address out of the admin list, save, and sign in again. You still get into the app; the admin page now turns you away. That’s the whole authorization model, made visible: everyone in the domain gets the default level, a named list gets more.
Look at what Google actually sent. Click See the raw claims. The hd field is the entire security boundary. Everything else is decoration.
Have a colleague try it. If you deployed it, send them the URL — and notice what you did not have to do. No invite, no account to create, no user list to add them to, no password to issue. They work here, so they’re in. That is the whole point, and it is the part worth showing someone rather than describing.
When “Internal” isn’t available
If the Internal option is greyed out, or your project has no organization, your Workspace tenant has never been linked to Google Cloud. Two ways forward:
Link it (right answer, needs the Workspace super-admin): in the Cloud console the org is created automatically the first time a super-admin from your domain visits console.cloud.google.com and accepts the terms. Then recreate the project.
Use External instead (fine for a demo): choose External audience and publish the app to Production. Because you’re only asking for those three basic scopes, Google doesn’t require a verification review. The difference that matters: Google will now let anyone with a Google account reach your consent screen, so the Workspace-domain check in the app becomes the only thing keeping strangers out. It works — it’s just one lock instead of two.
When it doesn’t work
| What you see | What it means |
|---|---|
redirect_uri_mismatch |
The URI registered in Google isn’t byte-identical to the one the app shows on /setup. Re-copy it from the app rather than retyping. If you deployed after registering, check the hostname didn’t change. |
invalid_client |
Client ID or secret is wrong, or picked up a stray space or line break when pasted. Re-paste both. |
invalid_grant |
You refreshed the callback page. Sign-in codes are single-use — start over from the home page. |
Access blocked: … has not completed the Google verification process |
Your audience is External and still in Testing. Publish it to Production, or switch to Internal. |
| That setup token is not right | The token is the app’s own, not Google’s and not Cloudflare’s. Whoever deployed the app set it; they can set a new one with wrangler secret put SETUP_TOKEN. |
| Signed in, then bounced straight back to the sign-in page | The session cookie isn’t sticking. Check the app is being reached over https on its real hostname, not through a preview or tunnel URL that differs from the registered redirect URI. |
| Not allowed in, but it’s the right account | The Workspace domain on /setup doesn’t match your real primary domain. Check the hd value on the error page — that’s the truth. Alias domains differ from the primary. |
Taking this to a real app
Nothing about the Google side changes. Adding a domain later means registering one more redirect URI on the same client — that is the entire migration.
Two things are worth changing in the code:
- Roles move out of the setup screen into a users table, one row per person, so an admin edits them from a screen instead of retyping a list. The demo keeps them in one field to stay legible.
- The setup screen itself goes away, or becomes an admin-only page, once the app has a real user model. It exists to solve a first-run problem: getting credentials in without shipping them in the code.
Two things are already right and shouldn’t be undone:
- Sessions live in shared storage, not in memory. On a distributed runtime an in-memory session store produces random logouts that look exactly like a broken login.
- The
hdcheck is pinned to one domain. Accepting any Workspace — which is what checking merely thathdexists does — lets in every Google Workspace on earth.
The sign-in flow itself — the four steps in the callback — is the same code you’d ship.