Repository navigation
Add lib/auth/oidcgoogle: a Google-only OIDCService implementation - #5
Open
neilLasrado wants to merge 1 commit into
Open
neilLasrado wants to merge 1 commit into
neilLasrado wants to merge 1 commit into
Conversation
Implements auth.OIDCService so a self-hosted cluster can authenticate users against Google Workspace, restricted to a single hosted domain. Nothing registers the service yet; that is a later slice. Authorization rests on the hd claim: it must be present and exactly equal to the configured domain, compared as ASCII-lowercase. A personal Gmail account carries no hd at all, so absence is an explicit rejection rather than a default. The domain is never derived from the email claim, which a suffix match would make forgeable via example.com.evil.net. The ID token is fully verified before any claim is trusted, with signing algorithms pinned to RS256 and the nonce bound to the state token as hex(SHA-256(state)). State is single-use: deleted on read, failing closed if the delete errors. Two guards protect existing accounts. A Google identity cannot take over a user created by another connector, and cannot inherit a user whose stored OIDC sub differs -- Google never reuses sub, but an email address can be reissued to a different person. checkConnector refuses five settings this implementation does not enforce rather than ignoring them: non-Google issuer, allow_unverified_email, max_age, Workspace directory sync, and mfa.enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
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.
Slice 3 of 6. Stacked on #3 (
feat/oidc-slice1-delete-auth-request) because it needsDeleteOIDCAuthRequest. Merge #3 first. Nothing registers the service yet, so merging this changes no behaviour.What it does
Implements
auth.OIDCServicefor Google Workspace, restricted to one hosted domain.The authorization decision is
checkClaims— requirehdpresent and exactly equal to the configured domain (ASCII-lowercase comparison), andemail_verified == true. A personal Gmail carries nohdclaim at all, so absence is an explicit rejection, never a default-allow. The domain is never derived fromemail; a suffix match there is forgeable viaexample.com.evil.net.Before any claim is trusted, the ID token is fully verified — signature,
iss,aud,exp, nonce — with signing algorithms pinned to RS256. The nonce is bound to the state token ashex(SHA-256(state)). State is single-use: deleted on read, failing closed if the delete errors.Two guards protect existing accounts. A Google identity cannot take over a user created by a different connector (upstream's rule), and cannot inherit a user whose stored OIDC
subdiffers — Google never reusessub, but an email address can be reissued to a new person.checkConnectorrefuses five settings this implementation doesn't enforce, rather than ignoring them: non-Google issuer,allow_unverified_email,max_age, Workspace directory sync,mfa.enabled. Silently ignoring an operator's setting is how a control quietly stops working.Independent review
A second agent verified every security property against the source with line numbers, and confirmed each:
hdhandling, no email-derived domain,email_verified, verify-before-trust, algorithm pinning, state single-use, nonce derivation, both collision guards,SSOTestFlowwriting no user, audit events on success and failure, fail-closed constructor. Verdict: ready for a human, with the items below.Known items, carried deliberately
1.
ErrUnauthorizedIdentitydoes not survive the gRPC boundary. The original comment claimed the proxy would match it witherrors.Isto show an "unauthorized" page. It can't: the callback runs on Auth, and a trace error crossing gRPC is flattened to a code plus message and rebuilt client-side, so anerrors.Newsentinel never matches. (ErrGithubNoTeamsworks only because it is atrace.BadParameter, whoseIscompares message strings.) The comment is corrected in this PR. Effect is a UX shortfall — unauthorized identities get the generic failure page — not a security one. The rejection still happens on Auth and the audit event still carries the reason. Follow-up needed for the distinct page.2.
ValidateClientRedirectis skipped whenCreateWebSessionis true. This replicateslib/auth/github.go:145-158exactly. Reviewer confirmed it is not attacker-influenced:auth_with_roles.go:4560-4562restrictsCreateWebSessionto callers holdingRoleProxy, and the proxy passes the redirect throughhttplib.OriginLocalRedirectURI, which strips it to a relative path. Certificates never reach that URL. This is the highest-consequence judgement call in the slice and deserves a human signing off on the inherited guarantee.3.
Get+Deleteof the auth request are not atomic. Two concurrent callbacks could both pass theGet. In practice the loser'sDeletereturnsNotFoundand fails closed, and both would hold the samecode, which Google rejects on second exchange. A compare-and-delete would close it fully; that is a change to #3, deliberately not made here.4.
ConnectorSpecincluding the client secret is persisted forSSOTestFlow. Upstream behaviour —OIDCAuthRequest.Check()requires it — and only a connector admin can trigger it. Flagged as a conscious inheritance rather than an accident.5. Test gaps. 64 tests pass, but there is no negative test for a non-pinned signing algorithm: the fake issuer signs ES256 and tests override the pin to match, so
WithSupportedSigningAlgorithmsis never exercised negatively. Also missing:hdpresent-but-empty, multi-audience with badazp, and a regression test thathdactually unmarshals (see below).One trap worth knowing about
googleClaimsdeliberately does not embedzoidc.IDTokenClaims. That type defines its ownUnmarshalJSON, which Go would promote onto the outer struct, leaving every declared field silently zero —hdpermanently empty. It fails closed (everyone rejected), but a future "tidy-up" would break login with no obvious cause. There is currently no test that would catch it.Verification
GOFLAGS=-p=2 GOMAXPROCS=2, scoped, never./...:go build ./lib/auth/oidcgoogle/— OKgo test ./lib/auth/oidcgoogle/ -count=1— 64/64 passgo test ./lib/oidc/... ./lib/kube/token/ ./lib/join/— pass (existing users of thefakeissuerhelper, which is extended additively)go vet ./lib/auth/— cleanlib/oidc/fakeissuerneeded extending: as shipped its discovery document advertises no token endpoint and its only signing method is hardcoded to Kubernetes claims.SetEndpointsandSignClaimsare additive and no-ops for existing callers. Discovery, JWKS and signature verification in the tests are real; only the token endpoint is hosted by the test.Also written in-package: claims→traits conversion, because
services.OIDCClaimsToTraitsis Enterprise-only and not in this tree.Suggested reading order for the human reviewer
checkClaimsandclaims.go— the entire authorization decision, ~40 linesexchangeAndVerify— confirm for yourself that omittingrp.WithNoncewould assert an empty nonce rather than skip the check. Reviewer verified this againstrp/verifier.go:76-80,119-121; it is load-bearing and it fails closed.createUser— the two collision guards. Decide whether a user with an empty storedUserIDshould be silently upgradeable.auth_with_roles.go:4560-4562andhttplib.go:300-318once and sign off.makeAuthResponse— a faithful mirror ofmakeGithubAuthResponse.🤖 Generated with Claude Code