diff options
author | Burcu Dogan <jbd@google.com> | 2014-11-24 17:07:50 -0800 |
---|---|---|
committer | Burcu Dogan <jbd@google.com> | 2014-11-25 14:36:49 -0800 |
commit | b8463885646621ad3debe22376c60031788d9f5e (patch) | |
tree | 8c572526823be65e998883ae1a858b89e5f7237e | |
parent | c048af9da2ff4db86f1ad989c73c6ec7a62c57a4 (diff) | |
download | golang-x-oauth2-b8463885646621ad3debe22376c60031788d9f5e.tar.gz |
oauth2: Removing the inconsistent and duplicate features, better naming
- Removed Flow, flow is a nothing but options.
- Renamed Cacher to Storer.
- Removed the setter from the Transport. Store should do the initial set.
Getter is not removed, because extra fields are available through
Transport.Token.Extra(). It's not pleasant to implement a custom Storer
implementation to read such values.
oauth2: Remove VMs from the AppEngine example title
-rw-r--r-- | example_test.go | 10 | ||||
-rw-r--r-- | google/appengine.go | 5 | ||||
-rw-r--r-- | google/example_test.go | 24 | ||||
-rw-r--r-- | oauth2.go | 126 | ||||
-rw-r--r-- | oauth2_test.go | 59 | ||||
-rw-r--r-- | transport.go | 23 |
6 files changed, 114 insertions, 133 deletions
diff --git a/example_test.go b/example_test.go index 52a3587..63ba707 100644 --- a/example_test.go +++ b/example_test.go @@ -18,7 +18,7 @@ import ( func TestA(t *testing.T) {} func Example_regular() { - f, err := oauth2.New( + opts, err := oauth2.New( oauth2.Client("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"), oauth2.RedirectURL("YOUR_REDIRECT_URL"), oauth2.Scope("SCOPE1", "SCOPE2"), @@ -33,7 +33,7 @@ func Example_regular() { // Redirect user to consent page to ask for permission // for the scopes specified above. - url := f.AuthCodeURL("state", "online", "auto") + url := opts.AuthCodeURL("state", "online", "auto") fmt.Printf("Visit the URL for the auth dialog: %v", url) // Use the authorization code that is pushed to the redirect URL. @@ -44,7 +44,7 @@ func Example_regular() { if _, err = fmt.Scan(&code); err != nil { log.Fatal(err) } - t, err := f.NewTransportFromCode(code) + t, err := opts.NewTransportFromCode(code) if err != nil { log.Fatal(err) } @@ -56,7 +56,7 @@ func Example_regular() { } func Example_jWT() { - f, err := oauth2.New( + opts, err := oauth2.New( // The contents of your RSA private key or your PEM file // that contains a private key. // If you have a p12 file instead, you @@ -82,6 +82,6 @@ func Example_jWT() { // Initiate an http.Client, the following GET request will be // authorized and authenticated on the behalf of user@example.com. - client := http.Client{Transport: f.NewTransport()} + client := http.Client{Transport: opts.NewTransport()} client.Get("...") } diff --git a/google/appengine.go b/google/appengine.go index 461fb88..1c47a07 100644 --- a/google/appengine.go +++ b/google/appengine.go @@ -7,6 +7,7 @@ package google import ( + "net/http" "strings" "sync" "time" @@ -44,7 +45,9 @@ func init() { func AppEngineContext(ctx appengine.Context) oauth2.Option { return func(opts *oauth2.Options) error { opts.TokenFetcherFunc = makeAppEngineTokenFetcher(ctx, opts) - opts.Transport = &urlfetch.Transport{Context: ctx} + opts.Client = &http.Client{ + Transport: &urlfetch.Transport{Context: ctx}, + } return nil } } diff --git a/google/example_test.go b/google/example_test.go index 54a5fd0..9213c66 100644 --- a/google/example_test.go +++ b/google/example_test.go @@ -24,7 +24,7 @@ func TestA(t *testing.T) {} func Example_webServer() { // Your credentials should be obtained from the Google // Developer Console (https://console.developers.google.com). - f, err := oauth2.New( + opts, err := oauth2.New( oauth2.Client("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"), oauth2.RedirectURL("YOUR_REDIRECT_URL"), oauth2.Scope( @@ -38,11 +38,11 @@ func Example_webServer() { } // Redirect user to Google's consent page to ask for permission // for the scopes specified above. - url := f.AuthCodeURL("state", "online", "auto") + url := opts.AuthCodeURL("state", "online", "auto") fmt.Printf("Visit the URL for the auth dialog: %v", url) // Handle the exchange code to initiate a transport - t, err := f.NewTransportFromCode("exchange-code") + t, err := opts.NewTransportFromCode("exchange-code") if err != nil { log.Fatal(err) } @@ -58,7 +58,7 @@ func Example_serviceAccountsJSON() { // To create a service account client, click "Create new Client ID", // select "Service Account", and click "Create Client ID". A JSON // key file will then be downloaded to your computer. - f, err := oauth2.New( + opts, err := oauth2.New( google.ServiceAccountJSONKey("/path/to/your-project-key.json"), oauth2.Scope( "https://www.googleapis.com/auth/bigquery", @@ -71,14 +71,14 @@ func Example_serviceAccountsJSON() { // Initiate an http.Client. The following GET request will be // authorized and authenticated on the behalf of // your service account. - client := http.Client{Transport: f.NewTransport()} + client := http.Client{Transport: opts.NewTransport()} client.Get("...") } func Example_serviceAccounts() { // Your credentials should be obtained from the Google // Developer Console (https://console.developers.google.com). - f, err := oauth2.New( + opts, err := oauth2.New( // The contents of your RSA private key or your PEM file // that contains a private key. // If you have a p12 file instead, you @@ -107,13 +107,13 @@ func Example_serviceAccounts() { // Initiate an http.Client, the following GET request will be // authorized and authenticated on the behalf of user@example.com. - client := http.Client{Transport: f.NewTransport()} + client := http.Client{Transport: opts.NewTransport()} client.Get("...") } -func Example_appEngineVMs() { +func Example_appEngine() { ctx := appengine.NewContext(nil) - f, err := oauth2.New( + opts, err := oauth2.New( google.AppEngineContext(ctx), oauth2.Scope( "https://www.googleapis.com/auth/bigquery", @@ -125,12 +125,12 @@ func Example_appEngineVMs() { } // The following client will be authorized by the App Engine // app's service account for the provided scopes. - client := http.Client{Transport: f.NewTransport()} + client := http.Client{Transport: opts.NewTransport()} client.Get("...") } func Example_computeEngine() { - f, err := oauth2.New( + opts, err := oauth2.New( // Query Google Compute Engine's metadata server to retrieve // an access token for the provided account. // If no account is specified, "default" is used. @@ -139,6 +139,6 @@ func Example_computeEngine() { if err != nil { log.Fatal(err) } - client := http.Client{Transport: f.NewTransport()} + client := http.Client{Transport: opts.NewTransport()} client.Get("...") } @@ -22,16 +22,16 @@ import ( "strings" ) -// Cacher implementations read and write OAuth 2.0 tokens from a cache. -type Cacher interface { - // Read reads the token from the cache. +// TokenStore implementations read and write OAuth 2.0 tokens from a persistence layer. +type TokenStore interface { + // ReadToken reads the token from the store. // If the read is successful, it should return the token and a nil error. // The returned tokens may be expired tokens. - // If there is no token in the cache, it should return a nil token and a nil error. + // If there is no token in the store, it should return a nil token and a nil error. // It should return a non-nil error when an unrecoverable failure occurs. - Read() (*Token, error) - // Write writes the token to the cache. - Write(*Token) + ReadToken() (*Token, error) + // WriteToken writes the token to the cache. + WriteToken(*Token) } // Option represents a function that applies some state to @@ -93,51 +93,27 @@ func HTTPClient(c *http.Client) Option { } } -// RoundTripper allows you to provide a custom http.RoundTripper -// to be used to construct new oauth2.Transport instances. -// If none is provided a default RoundTripper will be used. -func RoundTripper(tr http.RoundTripper) Option { - return func(o *Options) error { - o.Transport = tr - return nil - } -} - -// Cache requires a Cacher implementation. It will initially read -// the token if the transport is initialized with NewTransportFromCache -// and will write the refreshed tokens back to the cache. -func Cache(c Cacher) Option { - return func(o *Options) error { - o.Cache = c - return nil - } -} - -type Flow struct { - opts Options -} - -// New initiates a new flow. It determines the type of the OAuth 2.0 +// New builds a new options object and determines the type of the OAuth 2.0 // (2-legged, 3-legged or custom) by looking at the provided options. // If the flow type cannot determined automatically, an error is returned. -func New(options ...Option) (*Flow, error) { - f := &Flow{} - for _, opt := range options { - if err := opt(&f.opts); err != nil { +func New(option ...Option) (*Options, error) { + opts := &Options{} + for _, fn := range option { + if err := fn(opts); err != nil { return nil, err } } switch { - case f.opts.TokenFetcherFunc != nil: - return f, nil - case f.opts.AUD != nil: + case opts.TokenFetcherFunc != nil: + return opts, nil + case opts.AUD != nil: // TODO(jbd): Assert the required JWT params. - f.opts.TokenFetcherFunc = makeTwoLeggedFetcher(&f.opts) - return f, nil - case f.opts.AuthURL != nil && f.opts.TokenURL != nil: + opts.TokenFetcherFunc = makeTwoLeggedFetcher(opts) + return opts, nil + case opts.AuthURL != nil && opts.TokenURL != nil: // TODO(jbd): Assert the required OAuth2 params. - f.opts.TokenFetcherFunc = makeThreeLeggedFetcher(&f.opts) - return f, nil + opts.TokenFetcherFunc = makeThreeLeggedFetcher(opts) + return opts, nil default: return nil, errors.New("oauth2: missing endpoints, can't determine how to fetch tokens") } @@ -166,13 +142,13 @@ func New(options ...Option) (*Flow, error) { // granted consent and the code can only be exchanged for an // access token. If set to "force" the user will always be prompted, // and the code can be exchanged for a refresh token. -func (f *Flow) AuthCodeURL(state, accessType, prompt string) string { - u := *f.opts.AuthURL +func (o *Options) AuthCodeURL(state, accessType, prompt string) string { + u := *o.AuthURL v := url.Values{ "response_type": {"code"}, - "client_id": {f.opts.ClientID}, - "redirect_uri": condVal(f.opts.RedirectURL), - "scope": condVal(strings.Join(f.opts.Scopes, " ")), + "client_id": {o.ClientID}, + "redirect_uri": condVal(o.RedirectURL), + "scope": condVal(strings.Join(o.Scopes, " ")), "state": condVal(state), "access_type": condVal(accessType), "approval_prompt": condVal(prompt), @@ -188,55 +164,57 @@ func (f *Flow) AuthCodeURL(state, accessType, prompt string) string { // exchange exchanges the authorization code with the OAuth 2.0 provider // to retrieve a new access token. -func (f *Flow) exchange(code string) (*Token, error) { - return retrieveToken(&f.opts, url.Values{ +func (o *Options) exchange(code string) (*Token, error) { + return retrieveToken(o, url.Values{ "grant_type": {"authorization_code"}, "code": {code}, - "redirect_uri": condVal(f.opts.RedirectURL), - "scope": condVal(strings.Join(f.opts.Scopes, " ")), + "redirect_uri": condVal(o.RedirectURL), + "scope": condVal(strings.Join(o.Scopes, " ")), }) } -// NewTransportFromCache reads the token from the cache and returns +// NewTransportFromTokenStore reads the token from the store and returns // a Transport that is authorized and the authenticated // by the returned token. -func (f *Flow) NewTransportFromCache() (*Transport, error) { - if f.opts.Cache == nil { - return nil, errors.New("oauth2: no cache is set") - } - tok, err := f.opts.Cache.Read() +func (o *Options) NewTransportFromTokenStore(store TokenStore) (*Transport, error) { + tok, err := store.ReadToken() if err != nil { return nil, err } + o.TokenStore = store if tok == nil { return nil, nil } - return f.newTransportFromToken(tok), nil + return o.newTransportFromToken(tok), nil } // NewTransportFromCode exchanges the code to retrieve a new access token // and returns an authorized and authenticated Transport. -func (f *Flow) NewTransportFromCode(code string) (*Transport, error) { - token, err := f.exchange(code) +func (o *Options) NewTransportFromCode(code string) (*Transport, error) { + token, err := o.exchange(code) if err != nil { return nil, err } - return f.newTransportFromToken(token), nil + return o.newTransportFromToken(token), nil } // NewTransport returns a Transport. -func (f *Flow) NewTransport() *Transport { - return f.newTransportFromToken(nil) +func (o *Options) NewTransport() *Transport { + return o.newTransportFromToken(nil) } // newTransportFromToken returns a new Transport that is authorized // and authenticated with the provided token. -func (f *Flow) newTransportFromToken(t *Token) *Transport { - tr := f.opts.Transport - if tr == nil { - tr = http.DefaultTransport +func (o *Options) newTransportFromToken(t *Token) *Transport { + // TODO(jbd): App Engine options initiate an http.Client that + // depends on the urlfetcher, but it breaks the promise we made + // that the options object should be working finely with nil-values + // for the http.Client. + tr := http.DefaultTransport + if o.Client != nil && o.Client.Transport != nil { + tr = o.Client.Transport } - return newTransport(tr, &f.opts, t) + return newTransport(tr, o, t) } func makeThreeLeggedFetcher(o *Options) func(t *Token) (*Token, error) { @@ -294,12 +272,14 @@ type Options struct { // AUD represents the token endpoint required to complete the 2-legged JWT flow. AUD *url.URL - Cache Cacher + // TokenStore reads a token from the store and writes it back to the store + // if a token refresh occurs. + // Optional. + TokenStore TokenStore TokenFetcherFunc func(t *Token) (*Token, error) - Transport http.RoundTripper - Client *http.Client + Client *http.Client } func retrieveToken(o *Options, v url.Values) (*Token, error) { diff --git a/oauth2_test.go b/oauth2_test.go index d356f62..262e7be 100644 --- a/oauth2_test.go +++ b/oauth2_test.go @@ -25,38 +25,38 @@ type mockCache struct { readErr error } -func (c *mockCache) Read() (*Token, error) { +func (c *mockCache) ReadToken() (*Token, error) { return c.token, c.readErr } -func (c *mockCache) Write(*Token) { +func (c *mockCache) WriteToken(*Token) { // do nothing } -func newTestFlow(url string) *Flow { - f, _ := New( +func newOpts(url string) *Options { + opts, _ := New( Client("CLIENT_ID", "CLIENT_SECRET"), RedirectURL("REDIRECT_URL"), Scope("scope1", "scope2"), Endpoint(url+"/auth", url+"/token"), ) - return f + return opts } func TestAuthCodeURL(t *testing.T) { - f := newTestFlow("server") - url := f.AuthCodeURL("foo", "offline", "force") + opts := newOpts("server") + url := opts.AuthCodeURL("foo", "offline", "force") if url != "server/auth?access_type=offline&approval_prompt=force&client_id=CLIENT_ID&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=foo" { t.Errorf("Auth code URL doesn't match the expected, found: %v", url) } } func TestAuthCodeURL_Optional(t *testing.T) { - f, _ := New( + opts, _ := New( Client("CLIENT_ID", ""), Endpoint("auth-url", "token-token"), ) - url := f.AuthCodeURL("", "", "") + url := opts.AuthCodeURL("", "", "") if url != "auth-url?client_id=CLIENT_ID&response_type=code" { t.Fatalf("Auth code URL doesn't match the expected, found: %v", url) } @@ -86,8 +86,8 @@ func TestExchangeRequest(t *testing.T) { w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer")) })) defer ts.Close() - f := newTestFlow(ts.URL) - tr, err := f.NewTransportFromCode("exchange-code") + opts := newOpts(ts.URL) + tr, err := opts.NewTransportFromCode("exchange-code") if err != nil { t.Error(err) } @@ -131,8 +131,8 @@ func TestExchangeRequest_JSONResponse(t *testing.T) { w.Write([]byte(`{"access_token": "90d64460d14870c08c81352a05dedd3465940a7c", "scope": "user", "token_type": "bearer"}`)) })) defer ts.Close() - f := newTestFlow(ts.URL) - tr, err := f.NewTransportFromCode("exchange-code") + opts := newOpts(ts.URL) + tr, err := opts.NewTransportFromCode("exchange-code") if err != nil { t.Error(err) } @@ -158,8 +158,8 @@ func TestExchangeRequest_BadResponse(t *testing.T) { w.Write([]byte(`{"scope": "user", "token_type": "bearer"}`)) })) defer ts.Close() - f := newTestFlow(ts.URL) - tr, err := f.NewTransportFromCode("exchange-code") + opts := newOpts(ts.URL) + tr, err := opts.NewTransportFromCode("exchange-code") if err != nil { t.Error(err) } @@ -175,8 +175,8 @@ func TestExchangeRequest_BadResponseType(t *testing.T) { w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) })) defer ts.Close() - f := newTestFlow(ts.URL) - tr, err := f.NewTransportFromCode("exchange-code") + opts := newOpts(ts.URL) + tr, err := opts.NewTransportFromCode("exchange-code") if err != nil { t.Error(err) } @@ -197,12 +197,15 @@ func TestExchangeRequest_NonBasicAuth(t *testing.T) { }, } c := &http.Client{Transport: tr} - f, _ := New( + opts, err := New( Client("CLIENT_ID", ""), Endpoint("https://accounts.google.com/auth", "https://accounts.google.com/token"), HTTPClient(c), ) - f.NewTransportFromCode("code") + if err != nil { + t.Error(err) + } + opts.NewTransportFromCode("code") } func TestTokenRefreshRequest(t *testing.T) { @@ -223,9 +226,9 @@ func TestTokenRefreshRequest(t *testing.T) { } })) defer ts.Close() - f := newTestFlow(ts.URL) - tr := f.NewTransport() - tr.SetToken(&Token{RefreshToken: "REFRESH_TOKEN"}) + opts := newOpts(ts.URL) + tr := opts.NewTransport() + tr.token = &Token{RefreshToken: "REFRESH_TOKEN"} c := http.Client{Transport: tr} c.Get(ts.URL + "/somethingelse") } @@ -248,8 +251,8 @@ func TestFetchWithNoRefreshToken(t *testing.T) { } })) defer ts.Close() - f := newTestFlow(ts.URL) - tr := f.NewTransport() + opts := newOpts(ts.URL) + tr := opts.NewTransport() c := http.Client{Transport: tr} _, err := c.Get(ts.URL + "/somethingelse") if err == nil { @@ -258,12 +261,14 @@ func TestFetchWithNoRefreshToken(t *testing.T) { } func TestCacheNoToken(t *testing.T) { - f, _ := New( + opts, err := New( Client("CLIENT_ID", "CLIENT_SECRET"), Endpoint("/auth", "/token"), - Cache(&mockCache{token: nil, readErr: nil}), ) - tr, err := f.NewTransportFromCache() + if err != nil { + t.Error(err) + } + tr, err := opts.NewTransportFromTokenStore(&mockCache{token: nil, readErr: nil}) if err != nil { t.Errorf("No error expected, %v is found", err) } diff --git a/transport.go b/transport.go index 9df11d8..5062bd2 100644 --- a/transport.go +++ b/transport.go @@ -87,19 +87,19 @@ func newTransport(base http.RoundTripper, opts *Options, token *Token) *Transpor // RoundTrip authorizes and authenticates the request with an // access token. If no token exists or token is expired, // tries to refresh/fetch a new token. -func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - token := t.Token() +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + token := t.token if token == nil || token.Expired() { // Check if the token is refreshable. // If token is refreshable, don't return an error, // rather refresh. - if err := t.RefreshToken(); err != nil { + if err := t.refreshToken(); err != nil { return nil, err } - token = t.Token() - if t.opts.Cache != nil { - t.opts.Cache.Write(token) + token = t.token + if t.opts.TokenStore != nil { + t.opts.TokenStore.WriteToken(token) } } @@ -123,17 +123,10 @@ func (t *Transport) Token() *Token { return t.token } -// SetToken sets a token to the transport in a thread-safe way. -func (t *Transport) SetToken(v *Token) { - t.mu.Lock() - defer t.mu.Unlock() - t.token = v -} - -// RefreshToken retrieves a new token, if a refreshing/fetching +// refreshToken retrieves a new token, if a refreshing/fetching // method is known and required credentials are presented // (such as a refresh token). -func (t *Transport) RefreshToken() error { +func (t *Transport) refreshToken() error { t.mu.Lock() defer t.mu.Unlock() token, err := t.opts.TokenFetcherFunc(t.token) |