Repository: snowflakedb/gosnowflake
Pull request #1403 (2025-05-07 12:05:31)

SNOW-2089110: Add support for LOCAL_APPLICATION default client credentials

### Description

SNOW-2089110 Add support for LOCAL_APPLICATION default client credentials

### Checklist
- [x] Created tests which fail without the change (if possible)
- [x] Extended the README / documentation, if necessary



Patch:
diff --git a/auth_oauth.go b/auth_oauth.go
index 0453126..ea69673 100644
--- a/auth_oauth.go
+++ b/auth_oauth.go
@@ -26,6 +26,7 @@ const (
 <body>
 OAuth authentication completed successfully.
 </body></html>`
+	localApplicationClientCredentials = "LOCAL_APPLICATION"
 )
 
 var defaultAuthorizationCodeProviderFactory = func() authorizationCodeProvider {
@@ -216,9 +217,13 @@ func (oauthClient *oauthClient) exchangeAccessToken(codeReq *http.Request, state
 }
 
 func (oauthClient *oauthClient) buildAuthorizationCodeConfig(callbackPort int) *oauth2.Config {
+	clientID, clientSecret := oauthClient.cfg.OauthClientID, oauthClient.cfg.OauthClientSecret
+	if oauthClient.eligibleForDefaultClientCredentials() {
+		clientID, clientSecret = localApplicationClientCredentials, localApplicationClientCredentials
+	}
 	return &oauth2.Config{
-		ClientID:     oauthClient.cfg.OauthClientID,
-		ClientSecret: oauthClient.cfg.OauthClientSecret,
+		ClientID:     clientID,
+		ClientSecret: clientSecret,
 		RedirectURL:  oauthClient.buildRedirectURI(callbackPort),
 		Scopes:       oauthClient.buildScopes(),
 		Endpoint: oauth2.Endpoint{
@@ -228,6 +233,14 @@ func (oauthClient *oauthClient) buildAuthorizationCodeConfig(callbackPort int) *
 		},
 	}
 }
+func (oauthClient *oauthClient) eligibleForDefaultClientCredentials() bool {
+	return oauthClient.cfg.OauthClientID == "" && oauthClient.cfg.OauthClientSecret == "" && oauthClient.isSnowflakeAsIDP()
+}
+
+func (oauthClient *oauthClient) isSnowflakeAsIDP() bool {
+	return (oauthClient.cfg.OauthAuthorizationURL == "" || strings.Contains(oauthClient.cfg.OauthAuthorizationURL, oauthClient.cfg.Host)) &&
+		(oauthClient.cfg.OauthTokenRequestURL == "" || strings.Contains(oauthClient.cfg.OauthTokenRequestURL, oauthClient.cfg.Host))
+}
 
 func (oauthClient *oauthClient) authorizationURL() string {
 	return cmp.Or(oauthClient.cfg.OauthAuthorizationURL, oauthClient.defaultAuthorizationURL())


Test patch:
diff --git a/auth_oauth_test.go b/auth_oauth_test.go
index 019c6e6..08f2e1a 100644
--- a/auth_oauth_test.go
+++ b/auth_oauth_test.go
@@ -562,6 +562,89 @@ func TestClientCredentialsFlow(t *testing.T) {
 	})
 }
 
+func TestEligibleForDefaultClientCredentials(t *testing.T) {
+	tests := []struct {
+		name        string
+		oauthClient *oauthClient
+		expected    bool
+	}{
+		{
+			name: "Client credentials not supplied and Snowflake as IdP",
+			oauthClient: &oauthClient{
+				cfg: &Config{
+					Host:                  "example.snowflakecomputing.com",
+					OauthClientID:         "",
+					OauthClientSecret:     "",
+					OauthAuthorizationURL: "https://example.snowflakecomputing.com/oauth/authorize",
+					OauthTokenRequestURL:  "https://example.snowflakecomputing.com/oauth/token",
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Client credentials not supplied and empty URLs (defaults to Snowflake)",
+			oauthClient: &oauthClient{
+				cfg: &Config{
+					Host:                  "example.snowflakecomputing.com",
+					OauthClientID:         "",
+					OauthClientSecret:     "",
+					OauthAuthorizationURL: "",
+					OauthTokenRequestURL:  "",
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Client credentials supplied",
+			oauthClient: &oauthClient{
+				cfg: &Config{
+					Host:                  "example.snowflakecomputing.com",
+					OauthClientID:         "testClientID",
+					OauthClientSecret:     "testClientSecret",
+					OauthAuthorizationURL: "https://example.snowflakecomputing.com/oauth/authorize",
+					OauthTokenRequestURL:  "https://example.snowflakecomputing.com/oauth/token",
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "Only client ID supplied",
+			oauthClient: &oauthClient{
+				cfg: &Config{
+					Host:                  "example.snowflakecomputing.com",
+					OauthClientID:         "testClientID",
+					OauthClientSecret:     "",
+					OauthAuthorizationURL: "https://example.snowflakecomputing.com/oauth/authorize",
+					OauthTokenRequestURL:  "https://example.snowflakecomputing.com/oauth/token",
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "Non-Snowflake IdP",
+			oauthClient: &oauthClient{
+				cfg: &Config{
+					Host:                  "example.snowflakecomputing.com",
+					OauthClientID:         "",
+					OauthClientSecret:     "",
+					OauthAuthorizationURL: "https://example.com/oauth/authorize",
+					OauthTokenRequestURL:  "https://example.com/oauth/token",
+				},
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := test.oauthClient.eligibleForDefaultClientCredentials()
+			if result != test.expected {
+				t.Errorf("expected %v, got %v", test.expected, result)
+			}
+		})
+	}
+}
+
 type nonInteractiveAuthorizationCodeProvider struct {
 	t               *testing.T
 	tamperWithState bool