mirror of
https://github.com/cubixle/ebay.git
synced 2026-04-24 22:54:42 +01:00
add clientcredentials pkg for oauth2
This commit is contained in:
121
clientcredentials/clientcredentials.go
Normal file
121
clientcredentials/clientcredentials.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Package clientcredentials implements the eBay client credentials grant flow
|
||||||
|
// to generate "Application access" tokens.
|
||||||
|
// eBay doc: https://developer.ebay.com/api-docs/static/oauth-client-credentials-grant.html
|
||||||
|
//
|
||||||
|
// The only difference from the original golang.org/x/oauth2/clientcredentials is the token type
|
||||||
|
// being forced to "Bearer". The eBay api /identity/v1/oauth2/token endpoint returns
|
||||||
|
// "Application Access Token" as token type which is then reused:
|
||||||
|
// https://github.com/golang/oauth2/blob/aaccbc9213b0974828f81aaac109d194880e3014/token.go#L68-L70
|
||||||
|
package clientcredentials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// ClientID is the application's ID.
|
||||||
|
ClientID string
|
||||||
|
|
||||||
|
// ClientSecret is the application's secret.
|
||||||
|
ClientSecret string
|
||||||
|
|
||||||
|
// TokenURL is the resource server's token endpoint
|
||||||
|
// URL. This is a constant specific to each server.
|
||||||
|
TokenURL string
|
||||||
|
|
||||||
|
// Scope specifies optional requested permissions.
|
||||||
|
Scopes []string
|
||||||
|
|
||||||
|
// HTTPClient used to make requests.
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token uses client credentials to retrieve a token.
|
||||||
|
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
||||||
|
return c.TokenSource(ctx).Token()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns an HTTP client using the provided token.
|
||||||
|
// The token will auto-refresh as necessary.
|
||||||
|
// The returned client and its Transport should not be modified.
|
||||||
|
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||||
|
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenSource returns a TokenSource that returns t until t expires,
|
||||||
|
// automatically refreshing it as necessary using the provided context and the
|
||||||
|
// client ID and client secret.
|
||||||
|
//
|
||||||
|
// Most users will use Config.Client instead.
|
||||||
|
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||||
|
source := &tokenSource{
|
||||||
|
ctx: ctx,
|
||||||
|
conf: c,
|
||||||
|
}
|
||||||
|
return oauth2.ReuseTokenSource(nil, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenSource struct {
|
||||||
|
ctx context.Context
|
||||||
|
conf *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token refreshes the token by using a new client credentials request.
|
||||||
|
// tokens received this way do not include a refresh token
|
||||||
|
func (c *tokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
v := url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
}
|
||||||
|
if len(c.conf.Scopes) > 0 {
|
||||||
|
v.Set("scope", strings.Join(c.conf.Scopes, " "))
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", c.conf.TokenURL, strings.NewReader(v.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth(url.QueryEscape(c.conf.ClientID), url.QueryEscape(c.conf.ClientSecret))
|
||||||
|
client := c.conf.HTTPClient
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
r, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot fetch token: %v", err)
|
||||||
|
}
|
||||||
|
if code := r.StatusCode; code < 200 || code > 299 {
|
||||||
|
return nil, fmt.Errorf("%s (%d)", req.URL, r.StatusCode)
|
||||||
|
}
|
||||||
|
token := struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
}{}
|
||||||
|
if err = json.NewDecoder(bytes.NewReader(body)).Decode(&token); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var expiry time.Time
|
||||||
|
if secs := token.ExpiresIn; secs > 0 {
|
||||||
|
expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
||||||
|
}
|
||||||
|
t := oauth2.Token{
|
||||||
|
AccessToken: token.AccessToken,
|
||||||
|
Expiry: expiry,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module github.com/jybp/ebay
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0
|
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0
|
||||||
)
|
)
|
||||||
|
|||||||
5
go.sum
5
go.sum
@@ -1,7 +1,10 @@
|
|||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -12,6 +15,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI
|
|||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0=
|
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0=
|
||||||
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
// +build integration
|
|
||||||
|
|
||||||
package ebay_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
"github.com/jybp/ebay"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"fmt"
|
|
||||||
"bytes"
|
|
||||||
"time"
|
|
||||||
"io/ioutil"
|
|
||||||
"encoding/json"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
var client *ebay.Client
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
clientID := os.Getenv("CLIENT_ID")
|
|
||||||
clientSecret := os.Getenv("CLIENT_SECRET")
|
|
||||||
if clientID == "" || clientSecret == "" {
|
|
||||||
panic("No CLIENT_ID or CLIENT_SECRET. Tests won't run.")
|
|
||||||
}
|
|
||||||
c := &http.Client{
|
|
||||||
Transport: &oauth2.Transport{
|
|
||||||
Source: oauth2.ReuseTokenSource(nil, TokenSource{
|
|
||||||
Endpoint: "https://api.sandbox.ebay.com/identity/v1/oauth2/token",
|
|
||||||
ID: clientID,
|
|
||||||
Secret: clientSecret,
|
|
||||||
Scopes: []string{"https://api.ebay.com/oauth/api_scope"},
|
|
||||||
Client: http.DefaultClient,
|
|
||||||
}),
|
|
||||||
Base: http.DefaultTransport,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client = ebay.NewSandboxClient(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenSource struct {
|
|
||||||
Endpoint string
|
|
||||||
ID string
|
|
||||||
Secret string
|
|
||||||
Scopes []string
|
|
||||||
Client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts TokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
scopes := strings.Join(ts.Scopes, " ")
|
|
||||||
req, err := http.NewRequest(http.MethodPost,
|
|
||||||
ts.Endpoint,
|
|
||||||
strings.NewReader(fmt.Sprintf("grant_type=client_credentials&scope=%s", url.PathEscape(scopes))))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.SetBasicAuth(ts.ID, ts.Secret)
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
resp, err := ts.Client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c := resp.StatusCode; c < 200 || c >= 300 {
|
|
||||||
return nil, fmt.Errorf("%s\nStatus:\n%d", req.URL, resp.StatusCode)
|
|
||||||
}
|
|
||||||
token := struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
}{}
|
|
||||||
if err = json.NewDecoder(bytes.NewReader(body)).Decode(&token); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t := oauth2.Token{
|
|
||||||
AccessToken: token.AccessToken,
|
|
||||||
TokenType: token.TokenType,
|
|
||||||
}
|
|
||||||
if secs := token.ExpiresIn; secs > 0 {
|
|
||||||
t.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
|
|
||||||
}
|
|
||||||
print(t.TokenType)
|
|
||||||
print("\n")
|
|
||||||
print(t.AccessToken)
|
|
||||||
print("\n")
|
|
||||||
return &t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthorization(t *testing.T) {
|
|
||||||
// TODO user token is reauired
|
|
||||||
req, err := client.NewRequest("GET", "buy/browse/v1/item_summary/search?q=drone&limit=3")
|
|
||||||
t.Log(req, err)
|
|
||||||
into := map[string]string{}
|
|
||||||
err = client.Do(context.Background(), req, &into)
|
|
||||||
t.Log(into, err)
|
|
||||||
}
|
|
||||||
45
test/integration/browse_test.go
Normal file
45
test/integration/browse_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// +build integration
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
"github.com/jybp/ebay"
|
||||||
|
"github.com/jybp/ebay/clientcredentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client *ebay.Client
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
clientID := os.Getenv("SANDBOX_CLIENT_ID")
|
||||||
|
clientSecret := os.Getenv("SANDBOX_CLIENT_SECRET")
|
||||||
|
if clientID == "" || clientSecret == "" {
|
||||||
|
panic("No SANDBOX_CLIENT_ID or SANDBOX_CLIENT_SECRET. Tests won't run.")
|
||||||
|
}
|
||||||
|
conf := clientcredentials.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
TokenURL: "https://api.sandbox.ebay.com/identity/v1/oauth2/token",
|
||||||
|
Scopes: []string{"https://api.ebay.com/oauth/api_scope"},
|
||||||
|
}
|
||||||
|
client = ebay.NewSandboxClient(conf.Client(context.Background()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorization(t *testing.T) {
|
||||||
|
// https://developer.ebay.com/my/api_test_tool?index=0&api=browse&call=item_summary_search__GET&variation=json
|
||||||
|
req, err := client.NewRequest("GET", "buy/browse/v1/item_summary/search?q=test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
into := map[string]interface{}{}
|
||||||
|
err = client.Do(context.Background(), req, &into)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if testing.Verbose() {
|
||||||
|
t.Log(into)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user