oauth2: fix expires_in for PayPal

PayPal returns "expires_in" token field as string, not integer.
So, current implementation cannot unmarshal json of tokenJSON due type mismatch.
This patch fixes the issue declaring field as interface{} in tokenJSON and performing type switch in "func (e *tokenJSON) expiry()".

Related to issue #41.

Change-Id: I69301e08c8a56fca049ca47906e32528cd22aef9
Reviewed-on: https://go-review.googlesource.com/6924
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/oauth2.go b/oauth2.go
index 4004642..4350a67 100644
--- a/oauth2.go
+++ b/oauth2.go
@@ -374,11 +374,11 @@
 // tokenJSON is the struct representing the HTTP response from OAuth2
 // providers returning a token in JSON form.
 type tokenJSON struct {
-	AccessToken  string `json:"access_token"`
-	TokenType    string `json:"token_type"`
-	RefreshToken string `json:"refresh_token"`
-	ExpiresIn    int32  `json:"expires_in"`
-	Expires      int32  `json:"expires"` // broken Facebook spelling of expires_in
+	AccessToken  string         `json:"access_token"`
+	TokenType    string         `json:"token_type"`
+	RefreshToken string         `json:"refresh_token"`
+	ExpiresIn    expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
+	Expires      expirationTime `json:"expires"`    // broken Facebook spelling of expires_in
 }
 
 func (e *tokenJSON) expiry() (t time.Time) {
@@ -391,6 +391,22 @@
 	return
 }
 
+type expirationTime int32
+
+func (e *expirationTime) UnmarshalJSON(b []byte) error {
+	var n json.Number
+	err := json.Unmarshal(b, &n)
+	if err != nil {
+		return err
+	}
+	i, err := n.Int64()
+	if err != nil {
+		return err
+	}
+	*e = expirationTime(i)
+	return nil
+}
+
 func condVal(v string) []string {
 	if v == "" {
 		return nil
diff --git a/oauth2_test.go b/oauth2_test.go
index 2ec482b..908a190 100644
--- a/oauth2_test.go
+++ b/oauth2_test.go
@@ -5,12 +5,16 @@
 package oauth2
 
 import (
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"reflect"
+	"strconv"
 	"testing"
+	"time"
 
 	"golang.org/x/net/context"
 )
@@ -159,6 +163,56 @@
 	}
 }
 
+const day = 24 * time.Hour
+
+func TestExchangeRequest_JSONResponse_Expiry(t *testing.T) {
+	seconds := int32(day.Seconds())
+	jsonNumberType := reflect.TypeOf(json.Number("0"))
+	for _, c := range []struct {
+		expires string
+		expect  error
+	}{
+		{fmt.Sprintf(`"expires_in": %d`, seconds), nil},
+		{fmt.Sprintf(`"expires_in": "%d"`, seconds), nil},                             // PayPal case
+		{fmt.Sprintf(`"expires": %d`, seconds), nil},                                  // Facebook case
+		{`"expires": false`, &json.UnmarshalTypeError{"bool", jsonNumberType}},        // wrong type
+		{`"expires": {}`, &json.UnmarshalTypeError{"object", jsonNumberType}},         // wrong type
+		{`"expires": "zzz"`, &strconv.NumError{"ParseInt", "zzz", strconv.ErrSyntax}}, // wrong value
+	} {
+		testExchangeRequest_JSONResponse_expiry(t, c.expires, c.expect)
+	}
+}
+
+func testExchangeRequest_JSONResponse_expiry(t *testing.T, exp string, expect error) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.Write([]byte(fmt.Sprintf(`{"access_token": "90d", "scope": "user", "token_type": "bearer", %s}`, exp)))
+	}))
+	defer ts.Close()
+	conf := newConf(ts.URL)
+	t1 := time.Now().Add(day)
+	tok, err := conf.Exchange(NoContext, "exchange-code")
+	t2 := time.Now().Add(day)
+	if err == nil && expect != nil {
+		t.Errorf("Incorrect state, conf.Exchange() should return an error: %v", expect)
+	} else if err != nil {
+		if reflect.DeepEqual(err, expect) {
+			t.Logf("Expected error: %v", err)
+			return
+		} else {
+			t.Error(err)
+		}
+
+	}
+	if !tok.Valid() {
+		t.Fatalf("Token invalid. Got: %#v", tok)
+	}
+	expiry := tok.Expiry
+	if expiry.Before(t1) || expiry.After(t2) {
+		t.Errorf("Unexpected value for Expiry: %v (shold be between %v and %v)", expiry, t1, t2)
+	}
+}
+
 func TestExchangeRequest_BadResponse(t *testing.T) {
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "application/json")