forked from golang/oauth2
-
Notifications
You must be signed in to change notification settings - Fork 0
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 golang#186 Change-Id: I3c6032899d6c286b84f8f24e0f6a240004f4f6c0
- Loading branch information
1 parent
c406a4c
commit 73daf54
Showing
2 changed files
with
203 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,101 @@ | ||
// 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" | ||
) | ||
|
||
// tokenFromInternal maps an *internal.Token struct into | ||
// an *oauth2.Token struct. | ||
func tokenFromInternal(t *internal.Token) *oauth2.Token { | ||
if t == nil { | ||
return nil | ||
} | ||
tk := &oauth2.Token{ | ||
AccessToken: t.AccessToken, | ||
TokenType: t.TokenType, | ||
RefreshToken: t.RefreshToken, | ||
Expiry: t.Expiry, | ||
} | ||
return tk.WithExtra(t.Raw) | ||
} | ||
|
||
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token. | ||
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along with an error. | ||
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*oauth2.Token, error) { | ||
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return tokenFromInternal(tk), nil | ||
} | ||
|
||
// 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) { | ||
return retrieveToken(c.ctx, c.conf, url.Values{ | ||
"grant_type": {"password"}, | ||
"username": {c.conf.Username}, | ||
"password": {c.conf.Password}, | ||
"scope": internal.CondVal(strings.Join(c.conf.Scopes, " ")), | ||
}) | ||
} |
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,102 @@ | ||
// 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 ( | ||
"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", | ||
}, | ||
} | ||
} | ||
|
||
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("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer")) | ||
})) | ||
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 != "90d64460d14870c08c81352a05dedd3465940a7c" { | ||
t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c") | ||
} | ||
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") | ||
} |