google/externalaccount: add Config.UniverseDomain

Change-Id: Ia1caee246da68c01addd06e1367ed1e43645826b
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/568216
Reviewed-by: Alex Eitzman <eitzman@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/google/default.go b/google/default.go
index 02ccd08..18f3698 100644
--- a/google/default.go
+++ b/google/default.go
@@ -22,7 +22,7 @@
 
 const (
 	adcSetupURL           = "https://cloud.google.com/docs/authentication/external/set-up-adc"
-	universeDomainDefault = "googleapis.com"
+	defaultUniverseDomain = "googleapis.com"
 )
 
 // Credentials holds Google credentials, including "Application Default Credentials".
@@ -58,7 +58,7 @@
 // See also [The attached service account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
 func (c *Credentials) UniverseDomain() string {
 	if c.universeDomain == "" {
-		return universeDomainDefault
+		return defaultUniverseDomain
 	}
 	return c.universeDomain
 }
@@ -89,7 +89,7 @@
 	// computeUniverseDomain that did not set universeDomain, set the default
 	// universe domain.
 	if c.universeDomain == "" {
-		c.universeDomain = universeDomainDefault
+		c.universeDomain = defaultUniverseDomain
 	}
 	return c.universeDomain, nil
 }
@@ -103,7 +103,7 @@
 	if err != nil {
 		if _, ok := err.(metadata.NotDefinedError); ok {
 			// http.StatusNotFound (404)
-			c.universeDomain = universeDomainDefault
+			c.universeDomain = defaultUniverseDomain
 			return nil
 		} else {
 			return err
@@ -287,7 +287,7 @@
 	}
 	// Authorized user credentials are only supported in the googleapis.com universe.
 	if f.Type == userCredentialsKey {
-		universeDomain = universeDomainDefault
+		universeDomain = defaultUniverseDomain
 	}
 
 	ts, err := f.tokenSource(ctx, params)
diff --git a/google/downscope/downscoping.go b/google/downscope/downscoping.go
index ca1f354..ebe8b05 100644
--- a/google/downscope/downscoping.go
+++ b/google/downscope/downscoping.go
@@ -51,7 +51,7 @@
 const (
 	universeDomainPlaceholder       = "UNIVERSE_DOMAIN"
 	identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token"
-	universeDomainDefault           = "googleapis.com"
+	defaultUniverseDomain           = "googleapis.com"
 )
 
 type accessBoundary struct {
@@ -117,7 +117,7 @@
 // configured universe domain.
 func (dc *DownscopingConfig) identityBindingEndpoint() string {
 	if dc.UniverseDomain == "" {
-		return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, universeDomainDefault, 1)
+		return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, defaultUniverseDomain, 1)
 	}
 	return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1)
 }
diff --git a/google/externalaccount/basecredentials.go b/google/externalaccount/basecredentials.go
index 71342e4..400aa0a 100644
--- a/google/externalaccount/basecredentials.go
+++ b/google/externalaccount/basecredentials.go
@@ -113,6 +113,7 @@
 	"net/http"
 	"regexp"
 	"strconv"
+	"strings"
 	"time"
 
 	"golang.org/x/oauth2"
@@ -120,6 +121,12 @@
 	"golang.org/x/oauth2/google/internal/stsexchange"
 )
 
+const (
+	universeDomainPlaceholder = "UNIVERSE_DOMAIN"
+	defaultTokenURL           = "https://sts.UNIVERSE_DOMAIN/v1/token"
+	defaultUniverseDomain     = "googleapis.com"
+)
+
 // now aliases time.Now for testing
 var now = func() time.Time {
 	return time.Now().UTC()
@@ -139,7 +146,9 @@
 	// Required.
 	SubjectTokenType string
 	// TokenURL is the STS token exchange endpoint. If not provided, will default to
-	// https://sts.googleapis.com/v1/token. Optional.
+	// https://sts.UNIVERSE_DOMAIN/v1/token, with UNIVERSE_DOMAIN set to the
+	// default service domain googleapis.com unless UniverseDomain is set.
+	// Optional.
 	TokenURL string
 	// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
 	// user attributes like account identifier, eg. email, username, uid, etc). This is
@@ -177,6 +186,10 @@
 	// AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials.
 	// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
 	AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
+	// UniverseDomain is the default service domain for a given Cloud universe.
+	// This value will be used in the default STS token URL. The default value
+	// is "googleapis.com". It will not be used if TokenURL is set. Optional.
+	UniverseDomain string
 }
 
 var (
@@ -246,9 +259,8 @@
 
 // Subject token file types.
 const (
-	fileTypeText    = "text"
-	fileTypeJSON    = "json"
-	defaultTokenUrl = "https://sts.googleapis.com/v1/token"
+	fileTypeText = "text"
+	fileTypeJSON = "json"
 )
 
 // Format contains information needed to retireve a subject token for URL or File sourced credentials.
@@ -336,11 +348,20 @@
 	SubjectTokenType string
 }
 
+// tokenURL returns the default STS token endpoint with the configured universe
+// domain.
+func (c *Config) tokenURL() string {
+	if c.UniverseDomain == "" {
+		return strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
+	}
+	return strings.Replace(defaultTokenURL, universeDomainPlaceholder, c.UniverseDomain, 1)
+}
+
 // parse determines the type of CredentialSource needed.
 func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
 	//set Defaults
 	if c.TokenURL == "" {
-		c.TokenURL = defaultTokenUrl
+		c.TokenURL = c.tokenURL()
 	}
 	supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType}
 
diff --git a/google/externalaccount/basecredentials_test.go b/google/externalaccount/basecredentials_test.go
index 5e896ee..33314c3 100644
--- a/google/externalaccount/basecredentials_test.go
+++ b/google/externalaccount/basecredentials_test.go
@@ -454,3 +454,46 @@
 		})
 	}
 }
+
+func TestConfig_TokenURL(t *testing.T) {
+	tests := []struct {
+		tokenURL       string
+		universeDomain string
+		want           string
+	}{
+		{
+			tokenURL:       "https://sts.googleapis.com/v1/token",
+			universeDomain: "",
+			want:           "https://sts.googleapis.com/v1/token",
+		},
+		{
+			tokenURL:       "",
+			universeDomain: "",
+			want:           "https://sts.googleapis.com/v1/token",
+		},
+		{
+			tokenURL:       "",
+			universeDomain: "googleapis.com",
+			want:           "https://sts.googleapis.com/v1/token",
+		},
+		{
+			tokenURL:       "",
+			universeDomain: "example.com",
+			want:           "https://sts.example.com/v1/token",
+		},
+	}
+	for _, tt := range tests {
+		config := &Config{
+			Audience:         "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
+			SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
+			CredentialSource: &testBaseCredSource,
+			Scopes:           []string{"https://www.googleapis.com/auth/devstorage.full_control"},
+		}
+		config.TokenURL = tt.tokenURL
+		config.UniverseDomain = tt.universeDomain
+		config.parse(context.Background())
+		if got := config.TokenURL; got != tt.want {
+			t.Errorf("got %q, want %q", got, tt.want)
+		}
+	}
+}