diff --git a/.nextchanges/cli/auth-token-error-code.md b/.nextchanges/cli/auth-token-error-code.md new file mode 100644 index 00000000000..919318cf952 --- /dev/null +++ b/.nextchanges/cli/auth-token-error-code.md @@ -0,0 +1 @@ +* Add an `INVALID_REFRESH_TOKEN` error code to `databricks auth token --output json` failures. ([#6681](https://github.com/databricks/cli/pull/6681)) diff --git a/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/output.txt b/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/output.txt index e3862605170..250f9863bfe 100644 --- a/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/output.txt +++ b/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/output.txt @@ -1,2 +1,6 @@ Error: A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: $ databricks auth login --profile test-profile +{ + "error_code": "INVALID_REFRESH_TOKEN", + "message": "A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command:\n $ databricks auth login --profile test-profile" +} diff --git a/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/script b/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/script index b1a76cafb1e..aedac8849e7 100644 --- a/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/script +++ b/acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/script @@ -2,3 +2,4 @@ setup_test_profile setup_test_token_cache musterr $CLI auth token --profile test-profile --force-refresh +musterr $CLI auth token --profile test-profile --force-refresh --output json diff --git a/cmd/auth/token.go b/cmd/auth/token.go index 300ebe8986c..09d8c2dabfe 100644 --- a/cmd/auth/token.go +++ b/cmd/auth/token.go @@ -25,6 +25,13 @@ import ( "golang.org/x/oauth2" ) +const invalidRefreshTokenErrorCode = "INVALID_REFRESH_TOKEN" + +type tokenErrorOutput struct { + ErrorCode string `json:"error_code"` + Message string `json:"message"` +} + func helpfulError(ctx context.Context, profile string, persistentAuth u2m.OAuthArgument) string { loginMsg := auth.BuildLoginCommand(ctx, profile, persistentAuth) return fmt.Sprintf("Try logging in again with `%s` before retrying. If this fails, please report this issue to the Databricks CLI maintainers at https://github.com/databricks/cli/issues/new", loginMsg) @@ -72,6 +79,14 @@ and secret is not supported.`, persistentAuthOpts: nil, }) if err != nil { + if cmd.Flag("output").Changed && root.OutputType(cmd) == flags.OutputJSON { + if _, ok := errors.AsType[*u2m.InvalidRefreshTokenError](err); ok { + if outputErr := writeTokenErrorOutput(cmd.OutOrStdout(), err); outputErr != nil { + return outputErr + } + return root.ErrAlreadyPrinted + } + } return err } // Only honor the explicit --output text flag, not implicit text mode @@ -98,6 +113,15 @@ func writeTokenOutput(w io.Writer, t *oauth2.Token, textMode bool) error { return err } +func writeTokenErrorOutput(w io.Writer, err error) error { + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(tokenErrorOutput{ + ErrorCode: invalidRefreshTokenErrorCode, + Message: err.Error(), + }) +} + type loadTokenArgs struct { // authArguments is the parsed auth arguments, including the host and optionally the account ID. authArguments *auth.AuthArguments diff --git a/cmd/auth/token_test.go b/cmd/auth/token_test.go index e6fdb58bcf1..18022d8ff5d 100644 --- a/cmd/auth/token_test.go +++ b/cmd/auth/token_test.go @@ -237,11 +237,12 @@ func TestToken_loadToken(t *testing.T) { } cases := []struct { - name string - setupCtx func(context.Context) context.Context - args loadTokenArgs - validateToken func(*oauth2.Token) - wantErr string + name string + setupCtx func(context.Context) context.Context + args loadTokenArgs + validateToken func(*oauth2.Token) + wantErr string + wantInvalidRefreshToken bool }{ { name: "prints helpful login message on refresh failure when profile is specified", @@ -260,6 +261,7 @@ func TestToken_loadToken(t *testing.T) { }, wantErr: `A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: $ databricks auth login --profile expired`, + wantInvalidRefreshToken: true, }, { name: "prints helpful login message on refresh failure when host is specified", @@ -281,6 +283,7 @@ func TestToken_loadToken(t *testing.T) { }, wantErr: `A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: $ databricks auth login --profile expired`, + wantInvalidRefreshToken: true, }, { name: "prints helpful login message on invalid response", @@ -893,6 +896,7 @@ func TestToken_loadToken(t *testing.T) { }, wantErr: `A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: $ databricks auth login --profile valid-token`, + wantInvalidRefreshToken: true, }, } for _, c := range cases { @@ -904,6 +908,8 @@ func TestToken_loadToken(t *testing.T) { got, err := loadToken(ctx, c.args) if c.wantErr != "" { assert.Equal(t, c.wantErr, err.Error()) + _, isInvalidRefreshToken := errors.AsType[*u2m.InvalidRefreshTokenError](err) + assert.Equal(t, c.wantInvalidRefreshToken, isInvalidRefreshToken) } else { assert.NoError(t, err) c.validateToken(got) @@ -948,3 +954,14 @@ func TestWriteTokenOutput(t *testing.T) { assert.Equal(t, "my-access-token\n", buf.String()) }) } + +func TestWriteTokenErrorOutput(t *testing.T) { + var buf bytes.Buffer + err := writeTokenErrorOutput(&buf, errors.New("refresh token is invalid")) + assert.NoError(t, err) + + var got tokenErrorOutput + assert.NoError(t, json.Unmarshal(buf.Bytes(), &got)) + assert.Equal(t, invalidRefreshTokenErrorCode, got.ErrorCode) + assert.Equal(t, "refresh token is invalid", got.Message) +} diff --git a/libs/auth/error.go b/libs/auth/error.go index d942a893ad4..fdb5f256f43 100644 --- a/libs/auth/error.go +++ b/libs/auth/error.go @@ -51,20 +51,33 @@ func AuthTypeDisplayName(authType string) string { return authType } +type rewrittenAuthError struct { + message string + cause error +} + +func (e *rewrittenAuthError) Error() string { + return e.message +} + +func (e *rewrittenAuthError) Unwrap() error { + return e.cause +} + // RewriteAuthError rewrites the error message for invalid refresh token error. // It returns whether the error was rewritten and the rewritten error. func RewriteAuthError(ctx context.Context, host, accountId, profile string, err error) (bool, error) { if _, ok := errors.AsType[*u2m.InvalidRefreshTokenError](err); ok { - oauthArgument, err := AuthArguments{ + oauthArgument, argErr := AuthArguments{ Host: host, AccountID: accountId, }.ToOAuthArgument() - if err != nil { - return false, err + if argErr != nil { + return false, argErr } msg := `A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: $ ` + BuildLoginCommand(ctx, profile, oauthArgument) - return true, errors.New(msg) + return true, &rewrittenAuthError{message: msg, cause: err} } return false, err }