From 7c39cf17752dcca966bd2e08b4fcc19cdaad8ea6 Mon Sep 17 00:00:00 2001 From: Jumpei Tsuji Date: Tue, 26 May 2015 23:42:30 +0900 Subject: [PATCH] Use JSON credential of service account instead of oauth token --- README.md | 11 +-- playstore/validator.go | 82 +++--------------- playstore/validator_test.go | 167 ++++++++---------------------------- 3 files changed, 56 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 2da2de5..1cbf715 100644 --- a/README.md +++ b/README.md @@ -54,13 +54,14 @@ import( ) func main() { - // You need to prepare an authorization code or a refresh token. - // If you have a refresh token, you can generate an oauth token like this. - token := &oauth2.Token{ - RefreshToken: "your refresh token", + // You need to prepare a public key for your Android app's in app billing + // at https://console.developers.google.com. + jsonKey, err := ioutil.ReadFile("jsonKey.json") + if err != nil { + log.Fatal(err) } - client := playstore.New(token) + client := playstore.New(jsonKey) resp, err := client.VerifySubscription("package", "subscriptionID", "purchaseToken") } ``` diff --git a/playstore/validator.go b/playstore/validator.go index 3a9eb1a..d56490d 100644 --- a/playstore/validator.go +++ b/playstore/validator.go @@ -1,86 +1,26 @@ package playstore import ( - "errors" "net/http" - "os" "time" "golang.org/x/net/context" "golang.org/x/oauth2" + "golang.org/x/oauth2/google" androidpublisher "google.golang.org/api/androidpublisher/v2" ) const ( - scope = "https://www.googleapis.com/auth/androidpublisher" - authURL = "https://accounts.google.com/o/oauth2/auth" - tokenURL = "https://accounts.google.com/o/oauth2/token" + scope = "https://www.googleapis.com/auth/androidpublisher" - timeout = time.Second * 5 + defaultTimeout = time.Second * 5 ) -var defaultConfig *oauth2.Config -var defaultTimeout = timeout - -// Init initializes the global configuration -func Init() error { - defaultConfig = &oauth2.Config{ - Scopes: []string{scope}, - Endpoint: oauth2.Endpoint{ - AuthURL: authURL, - TokenURL: tokenURL, - }, - } - - clientID := os.Getenv("IAB_CLIENT_ID") - if clientID != "" { - defaultConfig.ClientID = clientID - } - if defaultConfig.ClientID == "" { - return errors.New("Client ID is required") - } - - clientSecret := os.Getenv("IAB_CLIENT_SECRET") - if clientSecret != "" { - defaultConfig.ClientSecret = clientSecret - } - if defaultConfig.ClientSecret == "" { - return errors.New("Client Secret Key is required") - } - - return nil -} - -// InitWithConfig initializes the global configuration with parameters -func InitWithConfig(config *oauth2.Config) error { - if config.ClientID == "" { - return errors.New("Client ID is required") - } - - if config.ClientSecret == "" { - return errors.New("Client Secret Key is required") - } - - if len(config.Scopes) == 0 { - config.Scopes = []string{scope} - } - - if config.Endpoint.AuthURL == "" { - config.Endpoint.AuthURL = authURL - } - - if config.Endpoint.TokenURL == "" { - config.Endpoint.TokenURL = tokenURL - } - - defaultConfig = config - - return nil -} +var timeout = defaultTimeout // SetTimeout sets dial timeout duration func SetTimeout(t time.Duration) { - defaultTimeout = t + timeout = t } // The IABClient type is an interface to verify purchase token @@ -94,15 +34,17 @@ type Client struct { httpClient *http.Client } -// New returns http client which has oauth token -func New(token *oauth2.Token) Client { +// New returns http client which includes the credentials to access androidpublisher API. +// You should create a service account for your project at +// https://console.developers.google.com and download a JSON key file to set this argument. +func New(jsonKey []byte) (Client, error) { ctx := context.WithValue(oauth2.NoContext, oauth2.HTTPClient, &http.Client{ - Timeout: defaultTimeout, + Timeout: timeout, }) - httpClient := defaultConfig.Client(ctx, token) + conf, err := google.JWTConfigFromJSON(jsonKey, scope) - return Client{httpClient} + return Client{conf.Client(ctx)}, err } // VerifySubscription Verifies subscription status diff --git a/playstore/validator_test.go b/playstore/validator_test.go index 84a318b..f33dba9 100644 --- a/playstore/validator_test.go +++ b/playstore/validator_test.go @@ -1,8 +1,8 @@ package playstore import ( + "encoding/json" "errors" - "os" "reflect" "testing" "time" @@ -10,146 +10,60 @@ import ( "golang.org/x/oauth2" ) -func TestInit(t *testing.T) { - expected := &oauth2.Config{ - ClientID: "dummyId", - ClientSecret: "dummySecret", - Scopes: []string{"https://www.googleapis.com/auth/androidpublisher"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", - }, - } - os.Setenv("IAB_CLIENT_ID", "dummyId") - os.Setenv("IAB_CLIENT_SECRET", "dummySecret") - Init() - os.Clearenv() - actual := defaultConfig - if !reflect.DeepEqual(actual, expected) { - t.Errorf("got %v\nwant %v", actual, expected) - } +type testSignature struct { + PrivateKeyID string `json:"private_key_id"` + PrivateKey string `json:"private_key"` + ClientEmail string `json:"client_email"` + ClientID string `json:"client_id"` + Type string `json:"type"` } -func TestInitWithoutClientSecret(t *testing.T) { - expected := errors.New("Client Secret Key is required") - - os.Setenv("IAB_CLIENT_ID", "dummyId") - actual := Init() - os.Clearenv() - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("got %v\nwant %v", actual, expected) - } -} - -func TestInitWithConfig(t *testing.T) { - expected := &oauth2.Config{ - ClientID: "dummyId", - ClientSecret: "dummySecret", - Scopes: []string{"https://www.googleapis.com/auth/androidpublisher"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", - }, - } - - config := &oauth2.Config{ - ClientID: "dummyId", - ClientSecret: "dummySecret", - } - - InitWithConfig(config) - actual := defaultConfig - if !reflect.DeepEqual(actual, expected) { - t.Errorf("got %v\nwant %v", actual, expected) - } -} - -func TestInitWithConfigErrors(t *testing.T) { - expected := errors.New("Client ID is required") - - config := &oauth2.Config{ - Scopes: []string{"https://www.googleapis.com/auth/androidpublisher"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", - }, - } - actual := InitWithConfig(config) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("got %v\nwant %v", actual, expected) - } - - expected = errors.New("Client Secret Key is required") - config = &oauth2.Config{ - ClientID: "dummyId", - Scopes: []string{"https://www.googleapis.com/auth/androidpublisher"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://accounts.google.com/o/oauth2/token", - }, - } - actual = InitWithConfig(config) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("got %v\nwant %v", actual, expected) - } +var testJSON = testSignature{ + PrivateKeyID: "dummyKeyID", + PrivateKey: "-----BEGIN PRIVATE KEY-----\nMIIBOQIBAAJBANXOa7wgs5KHMEVJmVo2eoRxEgeqiYF2oABPGYrebU+cQiE7Mwdy\nxv153DHME+9L9QzAj+fR4y5Rwva/fAsGAssCAwEAAQJATQwrFMtwCtC+22kvYywY\nsJuSlMKm9MmL1TCsErgfCj2rksRK1U+/ZY709tE3XJVYlZalWCeVhHTjs5p0pnk6\nYQIhAOw0FksytfIfpdfcREbful+LhFp1um5WjcVf7kQ73JDxAiEA57nJkG9pwnUd\nBCyIcElTVIAKU0+iFpd1208OnGxyT3sCIGaEBNkGXWmEytnxQ8DvAVjOmNcaGZwh\n/M4ZYLREtupBAiAsrpFkTWdqPKTcsi2Y4Tq1N39GMzvA+XGbWTIrDWo5UwIgHhp9\nEOnHuUuPCjpLfYM2vSFiYzaj8UJCImjkMtDwzbA=\n-----END PRIVATE KEY-----\n", + ClientEmail: "dummyEmail", + ClientID: "dummyClientID", + Type: "service_account", } func TestNew(t *testing.T) { - // Initialize config - _config := &oauth2.Config{ - ClientID: "dummyId", - ClientSecret: "dummySecret", - RedirectURL: "REDIRECT_URL", - Scopes: []string{"scope1", "scope2"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "http://example.com/auth", - TokenURL: "http://example.com/token", - }, - } - InitWithConfig(_config) + // Exception scenario + jsonKey, _ := json.Marshal(testJSON) + expected := "oauth2: cannot fetch token: 400 Bad Request\nResponse: {\n \"error\" : \"invalid_grant\"\n}" - _token := &oauth2.Token{ - AccessToken: "accessToken", - RefreshToken: "refreshToken", - Expiry: time.Unix(2234567890, 0).UTC(), - } - - actual := New(_token) + actual, _ := New(jsonKey) val := actual.httpClient.Transport.(*oauth2.Transport) - token, _ := val.Source.Token() - if !reflect.DeepEqual(token, _token) { - t.Errorf("got %v\nwant %v", token, _token) + token, err := val.Source.Token() + if token != nil { + t.Errorf("got %#v", token) } + if err.Error() != expected { + t.Errorf("got %v\nwant %v", err, expected) + } + + // TODO Normal scenario } func TestSetTimeout(t *testing.T) { - timeout := time.Second * 3 - SetTimeout(timeout) + _timeout := time.Second * 3 + SetTimeout(_timeout) - if defaultTimeout != timeout { - t.Errorf("got %#v\nwant %#v", defaultTimeout, timeout) + if timeout != _timeout { + t.Errorf("got %#v\nwant %#v", timeout, _timeout) } } func TestVerifySubscription(t *testing.T) { - Init() - // Exception scenario - token := &oauth2.Token{ - AccessToken: "accessToken", - RefreshToken: "refreshToken", - Expiry: time.Unix(2234567890, 0).UTC(), - } + jsonKey, _ := json.Marshal(testJSON) - client := New(token) - expected := "googleapi: Error 401: Invalid Credentials, authError" + expected := "Get https://www.googleapis.com/androidpublisher/v2/applications/package/purchases/subscriptions/subscriptionID/tokens/purchaseToken?alt=json: oauth2: cannot fetch token: 400 Bad Request\nResponse: {\n \"error\" : \"invalid_grant\"\n}" + + client, _ := New(jsonKey) _, err := client.VerifySubscription("package", "subscriptionID", "purchaseToken") if err.Error() != expected { - t.Errorf("got %v", err) + t.Errorf("got %v\nwant %v", err, expected) } // TODO Normal scenario @@ -166,17 +80,12 @@ func TestVerifySubscriptionAndroidPublisherError(t *testing.T) { } func TestVerifyProduct(t *testing.T) { - Init() - // Exception scenario - token := &oauth2.Token{ - AccessToken: "accessToken", - RefreshToken: "refreshToken", - Expiry: time.Unix(2234567890, 0).UTC(), - } + jsonKey, _ := json.Marshal(testJSON) - client := New(token) - expected := "googleapi: Error 401: Invalid Credentials, authError" + expected := "Get https://www.googleapis.com/androidpublisher/v2/applications/package/purchases/products/productID/tokens/purchaseToken?alt=json: oauth2: cannot fetch token: 400 Bad Request\nResponse: {\n \"error\" : \"invalid_grant\"\n}" + + client, _ := New(jsonKey) _, err := client.VerifyProduct("package", "productID", "purchaseToken") if err.Error() != expected {