Repository: nukosuke/go-zendesk
Pull request #246 (2022-12-07 23:05:18)

Add support for Bearer tokens
Zendesk OAuth tokens must use the Bearer prefix.

Question to maintainers: Can we bump the go.mod to 1.19? I tried doing it in my commits, but I think the appveyor build system might be using a Go binary of a lower version, I couldn't quite figure it out.


Patch:
diff --git a/appveyor.yml b/appveyor.yml
index fa110c0..6d6b35c 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -10,7 +10,7 @@ cache:
   - '%LocalAppData%\go-build'
   - '%GOPATH%\pkg\mod'
 
-stack: go 1.11
+stack: go 1.19
 
 install:
   - go mod download
diff --git a/go.mod b/go.mod
index d4cf90f..f275667 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
 module github.com/nukosuke/go-zendesk
 
+go 1.15
+
 require (
 	github.com/golang/mock v1.6.0
 	github.com/google/go-querystring v1.1.0
 )
-
-go 1.15
diff --git a/zendesk/credential.go b/zendesk/credential.go
index dd489af..aa7f280 100644
--- a/zendesk/credential.go
+++ b/zendesk/credential.go
@@ -4,6 +4,7 @@ package zendesk
 type Credential interface {
 	Email() string
 	Secret() string
+	Bearer() bool
 }
 
 // BasicAuthCredential is type of credential for Basic authentication
@@ -30,6 +31,11 @@ func (c BasicAuthCredential) Secret() string {
 	return c.password
 }
 
+// Bearer is accessor which returns whether the credential is a bearer token
+func (c BasicAuthCredential) Bearer() bool {
+	return false
+}
+
 // APITokenCredential is type of credential for API token authentication
 type APITokenCredential struct {
 	email    string
@@ -53,3 +59,34 @@ func (c APITokenCredential) Email() string {
 func (c APITokenCredential) Secret() string {
 	return c.apiToken
 }
+
+// Bearer is accessor which returns whether the credential is a bearer token
+func (c APITokenCredential) Bearer() bool {
+	return false
+}
+
+// BearerTokenCredential can be used to authenticate OAuth tokens issued by Zendesk.
+type BearerTokenCredential struct {
+	token string
+}
+
+// NewBearerTokenCredential returns a pointer to a new BearerTokenCredential. Bearer
+// tokens are issued by Zendesk OAuth clients.
+func NewBearerTokenCredential(token string) *BearerTokenCredential {
+	return &BearerTokenCredential{token: token}
+}
+
+// Email is accessor which returns email address
+func (c BearerTokenCredential) Email() string {
+	return ""
+}
+
+// Secret is accessor which returns API token
+func (c BearerTokenCredential) Secret() string {
+	return c.token
+}
+
+// Bearer is accessor which returns whether the credential is a bearer token
+func (c BearerTokenCredential) Bearer() bool {
+	return true
+}
diff --git a/zendesk/zendesk.go b/zendesk/zendesk.go
index 10e0d31..615526e 100644
--- a/zendesk/zendesk.go
+++ b/zendesk/zendesk.go
@@ -228,7 +228,11 @@ func (z *Client) prepareRequest(ctx context.Context, req *http.Request) *http.Re
 	out := req.WithContext(ctx)
 	z.includeHeaders(out)
 	if z.credential != nil {
-		out.SetBasicAuth(z.credential.Email(), z.credential.Secret())
+		if z.credential.Bearer() {
+			out.Header.Add("Authorization", "Bearer "+z.credential.Secret())
+		} else {
+			out.SetBasicAuth(z.credential.Email(), z.credential.Secret())
+		}
 	}
 
 	return out


Test patch:
diff --git a/zendesk/credential_test.go b/zendesk/credential_test.go
index f7c356a..90bf3f6 100644
--- a/zendesk/credential_test.go
+++ b/zendesk/credential_test.go
@@ -11,6 +11,9 @@ func TestNewBasicAuthCredential(t *testing.T) {
 	if cred.Secret() != "password" {
 		t.Fatalf("BasicAuthCredential: secret not match")
 	}
+	if cred.Bearer() {
+		t.Fatalf("BasicAuthCredential: not a bearer token")
+	}
 }
 
 func TestNewAPITokenCredential(t *testing.T) {
@@ -22,4 +25,21 @@ func TestNewAPITokenCredential(t *testing.T) {
 	if cred.Secret() != "apitoken" {
 		t.Fatalf("APITokenCredential: secret not match")
 	}
+	if cred.Bearer() {
+		t.Fatalf("APITokenCredential: not a bearer token")
+	}
+}
+
+func TestNewBearerTokenCredential(t *testing.T) {
+	cred := NewBearerTokenCredential("apitoken")
+
+	if cred.Email() != "" {
+		t.Fatalf("BearerTokenCredential: email not match")
+	}
+	if cred.Secret() != "apitoken" {
+		t.Fatalf("BearerTokenCredential: secret not match")
+	}
+	if !cred.Bearer() {
+		t.Fatalf("BearerTokenCredential: is a bearer token")
+	}
 }
diff --git a/zendesk/zendesk_test.go b/zendesk/zendesk_test.go
index 40d9384..d6d85f2 100644
--- a/zendesk/zendesk_test.go
+++ b/zendesk/zendesk_test.go
@@ -108,6 +108,24 @@ func TestSetCredential(t *testing.T) {
 	}
 }
 
+func TestBearerAuthCredential(t *testing.T) {
+	client, _ := NewClient(nil)
+	cred := NewBearerTokenCredential("hello")
+	client.SetCredential(cred)
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		auth := r.Header.Get("Authorization")
+		if auth != "Bearer hello" {
+			t.Fatalf("unexpected auth header: " + auth)
+		}
+	}))
+	client.SetEndpointURL(server.URL)
+	defer server.Close()
+
+	// trigger request, assert in the server code
+	_, _ = client.get(ctx, "/groups.json")
+}
+
 func TestGet(t *testing.T) {
 	mockAPI := newMockAPI(http.MethodGet, "groups.json")
 	client := newTestClient(mockAPI)