From 0a24246fcb8f55497359a134265aabe6662d37b4 Mon Sep 17 00:00:00 2001 From: Shivam-nagar23 Date: Thu, 11 Jun 2026 07:42:02 +0530 Subject: [PATCH] fix(google): allow "*" wildcard in hostedDomains to accept any hd claim When the Google connector is configured with hostedDomains: ["*"], the LoginURL already passes hd=* to Google to require a Workspace account, but the post-token claim check at createIdentity rejected the login with "oidc: unexpected hd claim " because the equality match in the loop never matched the literal "*" against a real domain. Extract the validation into a small matchHostedDomain helper and treat "*" as a wildcard that accepts any non-empty hd claim. This lets operators require any Workspace domain (i.e. block personal @gmail.com accounts) without enumerating every domain in config. Add unit tests covering exact match, wildcard, mixed wildcard + explicit entries, and the empty-claim case (wildcard does not accept an empty hd). Signed-off-by: Shivam-nagar23 --- connector/google/google.go | 31 +++++++++++------- connector/google/google_test.go | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/connector/google/google.go b/connector/google/google.go index 68bdf18ccd..fd84d88f18 100644 --- a/connector/google/google.go +++ b/connector/google/google.go @@ -168,6 +168,23 @@ func (c *googleConnector) Close() error { return nil } +// matchHostedDomain reports whether the given Google "hd" claim is allowed by +// the configured hostedDomains list. An entry of "*" acts as a wildcard that +// accepts any non-empty hd claim, allowing operators to require that users +// authenticate with a Workspace account (any domain) rather than a personal +// account, without enumerating every domain. +func matchHostedDomain(claim string, allowed []string) bool { + for _, domain := range allowed { + if domain == "*" && claim != "" { + return true + } + if claim == domain { + return true + } + } + return false +} + func (c *googleConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, []byte, error) { if c.redirectURI != callbackURL { return "", nil, fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) @@ -256,18 +273,8 @@ func (c *googleConnector) createIdentity(ctx context.Context, identity connector claims.Username = identity.Username } - if len(c.hostedDomains) > 0 { - found := false - for _, domain := range c.hostedDomains { - if claims.HostedDomain == domain { - found = true - break - } - } - - if !found { - return identity, fmt.Errorf("oidc: unexpected hd claim %v", claims.HostedDomain) - } + if len(c.hostedDomains) > 0 && !matchHostedDomain(claims.HostedDomain, c.hostedDomains) { + return identity, fmt.Errorf("oidc: unexpected hd claim %v", claims.HostedDomain) } var groups []string diff --git a/connector/google/google_test.go b/connector/google/google_test.go index ce0e017cf8..ad38eb9fdf 100644 --- a/connector/google/google_test.go +++ b/connector/google/google_test.go @@ -449,3 +449,61 @@ func TestPromptTypeConfig(t *testing.T) { }) } } + +func TestMatchHostedDomain(t *testing.T) { + tests := []struct { + name string + claim string + allowed []string + want bool + }{ + { + name: "exact match", + claim: "example.com", + allowed: []string{"example.com"}, + want: true, + }, + { + name: "exact match within multiple", + claim: "example.com", + allowed: []string{"foo.com", "example.com", "bar.com"}, + want: true, + }, + { + name: "no match", + claim: "ferrix.ai", + allowed: []string{"example.com"}, + want: false, + }, + { + name: "wildcard accepts any non-empty hd", + claim: "ferrix.ai", + allowed: []string{"*"}, + want: true, + }, + { + name: "wildcard alongside explicit domains", + claim: "anything.dev", + allowed: []string{"example.com", "*"}, + want: true, + }, + { + name: "wildcard rejects empty hd claim", + claim: "", + allowed: []string{"*"}, + want: false, + }, + { + name: "empty claim with explicit domains", + claim: "", + allowed: []string{"example.com"}, + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, matchHostedDomain(tc.claim, tc.allowed)) + }) + } +}