passwordcredentials: add

Using PasswordCredentialsToken requires a TokenSource. This implements a
Config similar to oauth2/clientcredentials for the Resource Owner Password
Credentials Grant.

See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.

Fixes https://github.com/golang/oauth2/issues/186

Change-Id: I3c6032899d6c286b84f8f24e0f6a240004f4f6c0
Reviewed-on: https://go-review.googlesource.com/23611
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/passwordcredentials/passwordcredentials.go b/passwordcredentials/passwordcredentials.go
new file mode 100644
index 0000000..5deef2e
--- /dev/null
+++ b/passwordcredentials/passwordcredentials.go
@@ -0,0 +1,90 @@
+// Copyright 2014 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 passwordcredentials implements the OAuth2.0 "password credentials" token flow.
+// See https://tools.ietf.org/html/rfc6749#section-4.3
+package passwordcredentials
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+
+	"golang.org/x/net/context"
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/internal"
+)
+
+// Config describes a Resource Owner Password Credentials OAuth2 flow, with the
+// client application information, resource owner credentials and the server's
+// endpoint URLs.
+type Config struct {
+	// ClientID is the application's ID.
+	ClientID string
+
+	// ClientSecret is the application's secret.
+	ClientSecret string
+
+	// Resource owner username
+	Username string
+
+	// Resource owner password
+	Password string
+
+	// Endpoint contains the resource server's token endpoint
+	// URLs. These are constants specific to each server and are
+	// often available via site-specific packages, such as
+	// google.Endpoint or github.Endpoint.
+	Endpoint oauth2.Endpoint
+
+	// Scope specifies optional requested permissions.
+	Scopes []string
+}
+
+// Client returns an HTTP client using the provided token.
+// The token will auto-refresh as necessary. The underlying
+// HTTP transport will be obtained using the provided context.
+// 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 password credentials request.
+// tokens received this way do not include a refresh token
+func (c *tokenSource) Token() (*oauth2.Token, error) {
+	tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.Endpoint.TokenURL, url.Values{
+		"grant_type": {"password"},
+		"username":   {c.conf.Username},
+		"password":   {c.conf.Password},
+		"scope":      internal.CondVal(strings.Join(c.conf.Scopes, " ")),
+	})
+	if err != nil {
+		return nil, err
+	}
+	t := &oauth2.Token{
+		AccessToken:  tk.AccessToken,
+		TokenType:    tk.TokenType,
+		RefreshToken: tk.RefreshToken,
+		Expiry:       tk.Expiry,
+	}
+	return t.WithExtra(tk.Raw), nil
+}
diff --git a/passwordcredentials/passwordcredentials_test.go b/passwordcredentials/passwordcredentials_test.go
new file mode 100644
index 0000000..f4cb19c
--- /dev/null
+++ b/passwordcredentials/passwordcredentials_test.go
@@ -0,0 +1,105 @@
+// Copyright 2014 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 passwordcredentials
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"golang.org/x/oauth2"
+)
+
+func newConf(url string) *Config {
+	return &Config{
+		ClientID:     "CLIENT_ID",
+		ClientSecret: "CLIENT_SECRET",
+		Username:     "USERNAME",
+		Password:     "PASSWORD",
+		Scopes:       []string{"scope1", "scope2"},
+		Endpoint: oauth2.Endpoint{
+			TokenURL: url + "/token",
+		},
+	}
+}
+
+const stubAccessToken = "90d64460d14870c08c81352a05dedd3465940a7c"
+
+type mockTransport struct {
+	rt func(req *http.Request) (resp *http.Response, err error)
+}
+
+func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
+	return t.rt(req)
+}
+
+func TestTokenRequest(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.String() != "/token" {
+			t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token")
+		}
+		headerAuth := r.Header.Get("Authorization")
+		if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" {
+			t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
+		}
+		if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
+			t.Errorf("Content-Type header = %q; want %q", got, want)
+		}
+		body, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			r.Body.Close()
+		}
+		if err != nil {
+			t.Errorf("failed reading request body: %s.", err)
+		}
+		want := "client_id=CLIENT_ID&grant_type=password&password=PASSWORD&scope=scope1+scope2&username=USERNAME"
+		if string(body) != want {
+			t.Errorf("payload = %q; want %q", string(body), want)
+		}
+		w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
+		w.Write([]byte(fmt.Sprintf("access_token=%s&token_type=bearer", stubAccessToken)))
+	}))
+	defer ts.Close()
+	conf := newConf(ts.URL)
+	tok, err := conf.TokenSource(oauth2.NoContext).Token()
+	if err != nil {
+		t.Error(err)
+	}
+	if !tok.Valid() {
+		t.Fatalf("token invalid. got: %#v", tok)
+	}
+	if tok.AccessToken != stubAccessToken {
+		t.Errorf("Access token = %q; want %q", tok.AccessToken, stubAccessToken)
+	}
+	if tok.TokenType != "bearer" {
+		t.Errorf("token type = %q; want %q", tok.TokenType, "bearer")
+	}
+}
+
+func TestTokenRefreshRequest(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.String() == "/somethingelse" {
+			return
+		}
+		if r.URL.String() != "/token" {
+			t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
+		}
+		headerContentType := r.Header.Get("Content-Type")
+		if headerContentType != "application/x-www-form-urlencoded" {
+			t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
+		}
+		body, _ := ioutil.ReadAll(r.Body)
+		want := "client_id=CLIENT_ID&grant_type=password&password=PASSWORD&scope=scope1+scope2&username=USERNAME"
+		if string(body) != want {
+			t.Errorf("payload = %q; want %q", string(body), want)
+		}
+	}))
+	defer ts.Close()
+	conf := newConf(ts.URL)
+	c := conf.Client(oauth2.NoContext)
+	c.Get(ts.URL + "/somethingelse")
+}