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") +}