Skip to content

Commit 6941fd1

Browse files
authored
feat(go): detect RAPT auth errors and auto-detect credential type (#141)
## What's Changed - Parse OAuth error responses in getAccessToken() instead of silently swallowing them - Detect RAPT (re-authentication policy) errors and surface actionable guidance to use service accounts or re-authenticate via gcloud - Extract shared isReauthError() helper and reauthGuidance constant to avoid duplication between connection.go and util.go - Auto-detect credential type from JSON file/string when not explicitly set, instead of passing empty/Unknown type to the Google SDK - Add debug-level logging for which auth path is active
1 parent bf36f2d commit 6941fd1

File tree

2 files changed

+93
-6
lines changed

2 files changed

+93
-6
lines changed

go/connection.go

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"io"
3232
"net/http"
3333
"net/url"
34+
"os"
3435
"regexp"
3536
"strconv"
3637
"strings"
@@ -266,10 +267,13 @@ func (c *connectionImpl) GetTablesForDBSchema(ctx context.Context, catalog strin
266267
}
267268

268269
type bigQueryTokenResponse struct {
269-
AccessToken string `json:"access_token"`
270-
ExpiresIn int `json:"expires_in"`
271-
Scope string `json:"scope"`
272-
TokenType string `json:"token_type"`
270+
AccessToken string `json:"access_token"`
271+
ExpiresIn int `json:"expires_in"`
272+
Scope string `json:"scope"`
273+
TokenType string `json:"token_type"`
274+
Error string `json:"error"`
275+
ErrorDescription string `json:"error_description"`
276+
ErrorURI string `json:"error_uri"`
273277
}
274278

275279
// GetCurrentCatalog implements driverbase.CurrentNamespacer.
@@ -712,9 +716,36 @@ func (c *connectionImpl) newClient(ctx context.Context) error {
712716
// First, establish base authentication
713717
switch c.authType {
714718
case OptionValueAuthTypeJSONCredentialFile:
715-
authOptions = append(authOptions, option.WithAuthCredentialsFile(c.credentialsType, c.credentials))
719+
credType := c.credentialsType
720+
if credType == "" {
721+
// Auto-detect credential type from the JSON file
722+
detected, err := detectCredentialTypeFromFile(c.credentials)
723+
if err != nil {
724+
return adbc.Error{
725+
Code: adbc.StatusInvalidArgument,
726+
Msg: fmt.Sprintf("[bq] failed to detect credential type from file %q: %s", c.credentials, err.Error()),
727+
}
728+
}
729+
credType = detected
730+
}
731+
c.credentialsType = credType
732+
c.Logger.Debug("Using JSON credential file", "file", c.credentials, "credentialType", string(credType))
733+
authOptions = append(authOptions, option.WithAuthCredentialsFile(credType, c.credentials))
716734
case OptionValueAuthTypeJSONCredentialString:
717-
authOptions = append(authOptions, option.WithAuthCredentialsJSON(c.credentialsType, []byte(c.credentials)))
735+
credType := c.credentialsType
736+
if credType == "" {
737+
detected, err := detectCredentialTypeFromJSON([]byte(c.credentials))
738+
if err != nil {
739+
return adbc.Error{
740+
Code: adbc.StatusInvalidArgument,
741+
Msg: fmt.Sprintf("[bq] failed to detect credential type from JSON: %s", err.Error()),
742+
}
743+
}
744+
credType = detected
745+
}
746+
c.credentialsType = credType
747+
c.Logger.Debug("Using JSON credential string", "credentialType", string(credType))
748+
authOptions = append(authOptions, option.WithAuthCredentialsJSON(credType, []byte(c.credentials)))
718749
case OptionValueAuthTypeUserAuthentication:
719750
if c.clientID == "" {
720751
return adbc.Error{
@@ -734,8 +765,10 @@ func (c *connectionImpl) newClient(ctx context.Context) error {
734765
Msg: fmt.Sprintf("[bq] `%s` parameter is empty", OptionStringAuthRefreshToken),
735766
}
736767
}
768+
c.Logger.Debug("Using user OAuth authentication")
737769
authOptions = append(authOptions, option.WithTokenSource(c))
738770
case OptionValueAuthTypeAppDefaultCredentials, OptionValueAuthTypeDefault, "":
771+
c.Logger.Debug("Using Application Default Credentials (ADC)", "authType", c.authType)
739772
// Use Application Default Credentials (default behavior)
740773
// No additional options needed - ADC is used by default
741774
default:
@@ -1146,5 +1179,44 @@ func (c *connectionImpl) getAccessToken() (*bigQueryTokenResponse, error) {
11461179
if err != nil {
11471180
return nil, errToAdbcErr(adbc.StatusIO, err, "get access token")
11481181
}
1182+
1183+
if tokenResponse.Error != "" {
1184+
msg := fmt.Sprintf("[bq] OAuth token error: %s", tokenResponse.Error)
1185+
if tokenResponse.ErrorDescription != "" {
1186+
msg += ": " + tokenResponse.ErrorDescription
1187+
}
1188+
if isReauthError(tokenResponse.ErrorDescription) {
1189+
msg += ". " + reauthGuidance
1190+
}
1191+
if tokenResponse.ErrorURI != "" {
1192+
msg += " (see: " + tokenResponse.ErrorURI + ")"
1193+
}
1194+
return nil, adbc.Error{
1195+
Code: adbc.StatusUnauthorized,
1196+
Msg: msg,
1197+
}
1198+
}
1199+
11491200
return &tokenResponse, nil
11501201
}
1202+
1203+
func detectCredentialTypeFromFile(filename string) (option.CredentialsType, error) {
1204+
data, err := os.ReadFile(filename)
1205+
if err != nil {
1206+
return "", fmt.Errorf("read credential file: %w", err)
1207+
}
1208+
return detectCredentialTypeFromJSON(data)
1209+
}
1210+
1211+
func detectCredentialTypeFromJSON(data []byte) (option.CredentialsType, error) {
1212+
var f struct {
1213+
Type string `json:"type"`
1214+
}
1215+
if err := json.Unmarshal(data, &f); err != nil {
1216+
return "", fmt.Errorf("parse credential JSON: %w", err)
1217+
}
1218+
if f.Type == "" {
1219+
return "", fmt.Errorf("missing 'type' field in credential JSON")
1220+
}
1221+
return option.CredentialsType(f.Type), nil
1222+
}

go/util.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,24 @@ func errToAdbcErr(defaultStatus adbc.Status, err error, errContext string, conte
226226
}
227227

228228
adbcErr.Msg = msg.String()
229+
230+
if isReauthError(err.Error()) {
231+
adbcErr.Code = adbc.StatusUnauthorized
232+
adbcErr.Msg += ". " + reauthGuidance
233+
}
234+
229235
return adbcErr
230236
}
231237

238+
const reauthGuidance = "Your Google Workspace admin requires re-authentication (RAPT). " +
239+
"Consider using a service account instead of user credentials, or re-authenticate " +
240+
"interactively with 'gcloud auth application-default login'. " +
241+
"See https://support.google.com/a/answer/9368756"
242+
243+
func isReauthError(s string) bool {
244+
return strings.Contains(s, "invalid_rapt") || strings.Contains(s, "reauth related error")
245+
}
246+
232247
func retryWithBackoff(ctx context.Context, context string, maxAttempts int, backoff gax.Backoff, f func() (bool, error)) error {
233248
attempt := 0
234249
for {

0 commit comments

Comments
 (0)