google: Wrap token sources in errWrappingTokenSource

Introduce new AuthenticationError type returned by
errWrappingTokenSource.Token. The new error wrapper
exposes a boolean method Temporary, identifying the
underlying network error as retryable based on the
following status codes: 500, 503, 408, or 429.

Bump go.mod version to 1.15

refs: https://github.com/googleapis/google-api-go-client/issues/1445

Change-Id: I27c76cb0c71b918c25a640f40d0bd515b2e488fc
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/403846
Reviewed-by: Cody Oss <codyoss@google.com>
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
diff --git a/go.mod b/go.mod
index 468b626..c95f33f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module golang.org/x/oauth2
 
-go 1.11
+go 1.15
 
 require (
 	cloud.google.com/go v0.65.0
diff --git a/google/default.go b/google/default.go
index dd00420..024a104 100644
--- a/google/default.go
+++ b/google/default.go
@@ -190,6 +190,7 @@
 	if err != nil {
 		return nil, err
 	}
+	ts = newErrWrappingTokenSource(ts)
 	return &DefaultCredentials{
 		ProjectID:   f.ProjectID,
 		TokenSource: ts,
diff --git a/google/error.go b/google/error.go
new file mode 100644
index 0000000..d84dd00
--- /dev/null
+++ b/google/error.go
@@ -0,0 +1,64 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package google
+
+import (
+	"errors"
+
+	"golang.org/x/oauth2"
+)
+
+// AuthenticationError indicates there was an error in the authentication flow.
+//
+// Use (*AuthenticationError).Temporary to check if the error can be retried.
+type AuthenticationError struct {
+	err *oauth2.RetrieveError
+}
+
+func newAuthenticationError(err error) error {
+	re := &oauth2.RetrieveError{}
+	if !errors.As(err, &re) {
+		return err
+	}
+	return &AuthenticationError{
+		err: re,
+	}
+}
+
+// Temporary indicates that the network error has one of the following status codes and may be retried: 500, 503, 408, or 429.
+func (e *AuthenticationError) Temporary() bool {
+	if e.err.Response == nil {
+		return false
+	}
+	sc := e.err.Response.StatusCode
+	return sc == 500 || sc == 503 || sc == 408 || sc == 429
+}
+
+func (e *AuthenticationError) Error() string {
+	return e.err.Error()
+}
+
+func (e *AuthenticationError) Unwrap() error {
+	return e.err
+}
+
+type errWrappingTokenSource struct {
+	src oauth2.TokenSource
+}
+
+func newErrWrappingTokenSource(ts oauth2.TokenSource) oauth2.TokenSource {
+	return &errWrappingTokenSource{src: ts}
+}
+
+// Token returns the current token if it's still valid, else will
+// refresh the current token (using r.Context for HTTP client
+// information) and return the new one.
+func (s *errWrappingTokenSource) Token() (*oauth2.Token, error) {
+	t, err := s.src.Token()
+	if err != nil {
+		return nil, newAuthenticationError(err)
+	}
+	return t, nil
+}
diff --git a/google/error_test.go b/google/error_test.go
new file mode 100644
index 0000000..cd60e91
--- /dev/null
+++ b/google/error_test.go
@@ -0,0 +1,111 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package google
+
+import (
+	"net/http"
+	"testing"
+
+	"golang.org/x/oauth2"
+)
+
+func TestAuthenticationError_Temporary(t *testing.T) {
+	tests := []struct {
+		name string
+		code int
+		want bool
+	}{
+		{
+			name: "temporary with 500",
+			code: 500,
+			want: true,
+		},
+		{
+			name: "temporary with 503",
+			code: 503,
+			want: true,
+		},
+		{
+			name: "temporary with 408",
+			code: 408,
+			want: true,
+		},
+		{
+			name: "temporary with 429",
+			code: 429,
+			want: true,
+		},
+		{
+			name: "temporary with 418",
+			code: 418,
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ae := &AuthenticationError{
+				err: &oauth2.RetrieveError{
+					Response: &http.Response{
+						StatusCode: tt.code,
+					},
+				},
+			}
+			if got := ae.Temporary(); got != tt.want {
+				t.Errorf("Temporary() = %v; want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestErrWrappingTokenSource_Token(t *testing.T) {
+	tok := oauth2.Token{AccessToken: "MyAccessToken"}
+	ts := errWrappingTokenSource{
+		src: oauth2.StaticTokenSource(&tok),
+	}
+	got, err := ts.Token()
+	if *got != tok {
+		t.Errorf("Token() = %v; want %v", got, tok)
+	}
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+type errTokenSource struct {
+	err error
+}
+
+func (s *errTokenSource) Token() (*oauth2.Token, error) {
+	return nil, s.err
+}
+
+func TestErrWrappingTokenSource_TokenError(t *testing.T) {
+	re := &oauth2.RetrieveError{
+		Response: &http.Response{
+			StatusCode: 500,
+		},
+	}
+	ts := errWrappingTokenSource{
+		src: &errTokenSource{
+			err: re,
+		},
+	}
+	_, err := ts.Token()
+	if err == nil {
+		t.Fatalf("errWrappingTokenSource.Token() err = nil, want *AuthenticationError")
+	}
+	ae, ok := err.(*AuthenticationError)
+	if !ok {
+		t.Fatalf("errWrappingTokenSource.Token() err = %T, want *AuthenticationError", err)
+	}
+	wrappedErr := ae.Unwrap()
+	if wrappedErr == nil {
+		t.Fatalf("AuthenticationError.Unwrap() err = nil, want *oauth2.RetrieveError")
+	}
+	_, ok = wrappedErr.(*oauth2.RetrieveError)
+	if !ok {
+		t.Errorf("AuthenticationError.Unwrap() err = %T, want *oauth2.RetrieveError", err)
+	}
+}
diff --git a/google/jwt.go b/google/jwt.go
index 67d97b9..e89e6ae 100644
--- a/google/jwt.go
+++ b/google/jwt.go
@@ -66,7 +66,8 @@
 	if err != nil {
 		return nil, err
 	}
-	return oauth2.ReuseTokenSource(tok, ts), nil
+	rts := newErrWrappingTokenSource(oauth2.ReuseTokenSource(tok, ts))
+	return rts, nil
 }
 
 type jwtAccessTokenSource struct {