feat(auth): support multiple Google hosted domains#605
Merged
Conversation
GOOGLE_HOSTED_DOMAIN now accepts a comma-separated list (e.g. "moe.edu.sg,hci.edu.sg"). verifyIdToken rejects any token whose hd claim is not one of the configured domains, and a missing hd claim is still rejected when any domain is set. The auth-URL hd hint passes the single domain when one is configured and falls back to "*" for multiple, since hd accepts only one value; enforcement remains in verifyIdToken (hd is a UI hint, not a security control). Related: #603 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add HostedDomainMismatchError (a subclass of InvalidIdTokenError) thrown when a token's hosted domain is not in GOOGLE_HOSTED_DOMAIN. The learner Google callback maps it to /login?error=domain_not_allowed (logged at warn, since an outside-domain sign-in is expected), and the login page renders an inline message telling the user to use an approved organisation account. Other verification failures still funnel to the generic oauth2_callback_failed with no banner. Related: #603 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address code-review feedback on #605: - Document that verifyIdToken enforces the restriction on the verified hd (Workspace) claim, not the email domain, so any account in an allow-listed Workspace is admitted regardless of its email's literal domain. No behavior change. - Generalize the login page so every error code surfaces a message: domain_not_allowed keeps its tailored text, and any other code (e.g. oauth2_callback_failed) now shows a generic retry message instead of a bare page. Related: #603 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nicholasjjlim
marked this pull request as ready for review
June 30, 2026 09:58
nicholasjjlim
commented
Jun 30, 2026
Collaborator
Author
Code Review Summary
One Low finding posted inline on What Looks Good
🤖 aif-code-review · claude-opus-4-8[1m] |
Google's hd claim is always lower-case, so a config like GOOGLE_HOSTED_DOMAIN=MOE.edu.sg would silently reject every user of that domain. Lower-case allow-list entries when parsing so matching is case-insensitive. Addresses code-review feedback on #605. Related: #603 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
LGTM 👍 🤖 aif-code-review · claude-opus-4-8[1m] |
santosral
approved these changes
Jun 30, 2026
Merged
wondopamine
added a commit
to transformteamsg/onward-design
that referenced
this pull request
Jul 1, 2026
The multi-hosted-domain auth work merged from main (String-dxd#605) added a login error notice styled with raw palette utilities (bg-red-50 / text-red-700), which token-audit [COL-2] flags. Re-point it to the semantic layer to match the app's canonical error-notice pattern (unit/[id] uses the same tokens): - bg-red-50 → bg-destructive/10 - text-red-700 → text-destructive Behaviour and layout unchanged; keeps the token-audit gate clean app-wide after the rebase onto v1.16.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WrZm9PZ693J1R2j5tV7cEF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related changes to Google sign-in:
GOOGLE_HOSTED_DOMAINnow accepts a comma-separated list of allowed Google Workspace domains (e.g.moe.edu.sg,hci.edu.sg). A single value behaves exactly as before.Related: #603
Changes
Multi-domain allowlist
getAllowedHostedDomains()— parsesGOOGLE_HOSTED_DOMAINby splitting on commas, trimming whitespace, and dropping empties. Returns[]when unset (no restriction).verifyIdToken(the security gate) — rejects unless the token's signedhdclaim is one of the configured domains; a missinghdclaim is still rejected when any domain is configured. Personal@gmail.comaccounts (nohdclaim) and other Workspace domains are rejected.generateAuthURL(UI hint only) — passes the single domain when one is configured, and falls back to*for multiple, since Google'shdparameter accepts only one value. Per Google's docs,hdis an account-chooser hint and not a security control — enforcement lives entirely inverifyIdToken.Domain-specific rejection message
HostedDomainMismatchError— a subclass ofInvalidIdTokenErrorthrown specifically for the hosted-domain mismatch (and missing-hd) case. Existing catch-all handling still treats it as a verification failure; callers that care can distinguish it.HostedDomainMismatchErrorto/login?error=domain_not_allowed(logged atwarn, since an outside-domain sign-in is expected behavior, not a fault). All other verification failures still funnel to the genericoauth2_callback_failed.errorparam and renders an inline message: "You can only sign in with an approved organisation account. Please try again with your work email."Testing
google.test.ts(10 tests): single / multiple / no configured domains for both functions, comma whitespace tolerance, missing-hdrejection, and that the rejection throwsHostedDomainMismatchError(and is still anInvalidIdTokenError).pnpm test run src/lib/server/auth/google.test.ts— 10 passedpnpm check— 0 errors, 0 warningsNotes / out of scope
verifyIdTokenbut still funnels all failures to its generic error; it could mirror the domain-specific message later if desired.