-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 #186 Change-Id: I3c6032899d6c286b84f8f24e0f6a240004f4f6c0 Reviewed-on: https://go-review.googlesource.com/23611 Reviewed-by: Andrew Gerrand <[email protected]>
- Loading branch information
1 parent
c406a4c
commit 71d9edd
Showing
2 changed files
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} |