From 7b9fe7a8a58666691cf8521546c58bf159ab6d10 Mon Sep 17 00:00:00 2001 From: JT Olds Date: Wed, 28 Dec 2016 17:25:59 -0700 Subject: [PATCH] proposal: app engine support App Engine unfortunately can't make outbound requests without using the appengine urlfetch library and a request-specific context. I don't see a way around this other than to change the signature of the Provider and Session interfaces. This really has a huge amount of downsides obviously, but before I go and fix stuff for providers besides Facebook, G+, and Twitter (Twitter support requires https://github.com/mrjones/oauth/pull/60 to get merged), I figured I'd open a pull request and discuss the tradeoffs. Unfortunately I think we need to do something like this to support App Engine. Perhaps this is an opportunity to version goth using gopkg.in or something? --- client.go | 15 ++++++++++ client_appengine.go | 16 +++++++++++ provider.go | 28 +++++++++++++++---- providers/facebook/facebook.go | 30 ++++++++++++++++---- providers/facebook/session.go | 13 ++++++--- providers/gplus/gplus.go | 39 ++++++++++++++++++++------ providers/gplus/session.go | 13 ++++++--- providers/twitter/session.go | 13 +++++++-- providers/twitter/twitter.go | 51 +++++++++++++++++++++++++++------- session.go | 19 ++++++++++--- 10 files changed, 191 insertions(+), 46 deletions(-) create mode 100644 client.go create mode 100644 client_appengine.go diff --git a/client.go b/client.go new file mode 100644 index 000000000..9caf87461 --- /dev/null +++ b/client.go @@ -0,0 +1,15 @@ +// +build !appengine + +package goth + +import ( + "net/http" + + "golang.org/x/net/context" +) + +// Provider implementations should use this method for making outbound HTTP +// requests. +var HTTPClient = func(ctx context.Context) (*http.Client, error) { + return http.DefaultClient, nil +} diff --git a/client_appengine.go b/client_appengine.go new file mode 100644 index 000000000..b3e0cca71 --- /dev/null +++ b/client_appengine.go @@ -0,0 +1,16 @@ +// +build appengine + +package goth + +import ( + "net/http" + + "golang.org/x/net/context" + "google.golang.org/appengine/urlfetch" +) + +// Provider implementations should use this method for making outbound HTTP +// requests. +var HTTPClient = func(ctx context.Context) (*http.Client, error) { + return urlfetch.Client(ctx), nil +} diff --git a/provider.go b/provider.go index 568fdc6ec..a629a0b1c 100644 --- a/provider.go +++ b/provider.go @@ -1,18 +1,34 @@ package goth -import "fmt" -import "golang.org/x/oauth2" +import ( + "fmt" + + "golang.org/x/net/context" + "golang.org/x/oauth2" +) // Provider needs to be implemented for each 3rd party authentication provider // e.g. Facebook, Twitter, etc... type Provider interface { + // When implementing a provider, these methods should not make outbound + // requests. Name() string - BeginAuth(state string) (Session, error) UnmarshalSession(string) (Session, error) - FetchUser(Session) (User, error) Debug(bool) - RefreshToken(refreshToken string) (*oauth2.Token, error) //Get new access token based on the refresh token - RefreshTokenAvailable() bool //Refresh token is provided by auth provider or not + // Refresh token is provided by auth provider or not + RefreshTokenAvailable() bool + + // These three methods are deprecated. See the appropriate *Ctx replacement. + BeginAuth(state string) (Session, error) + FetchUser(Session) (User, error) + RefreshToken(refreshToken string) (*oauth2.Token, error) + + // These methods are now preferred. + BeginAuthCtx(ctx context.Context, state string) (Session, error) + FetchUserCtx(context.Context, Session) (User, error) + // Get new access token based on the refresh token. + // Only works if RefreshTokenAvailable() is true + RefreshTokenCtx(ctx context.Context, refreshToken string) (*oauth2.Token, error) } // Providers is list of known/available providers. diff --git a/providers/facebook/facebook.go b/providers/facebook/facebook.go index 75f2ea543..52b8b4f55 100644 --- a/providers/facebook/facebook.go +++ b/providers/facebook/facebook.go @@ -8,10 +8,10 @@ import ( "errors" "io" "io/ioutil" - "net/http" "net/url" "github.com/markbates/goth" + "golang.org/x/net/context" "golang.org/x/oauth2" ) @@ -50,8 +50,12 @@ func (p *Provider) Name() string { // Debug is a no-op for the facebook package. func (p *Provider) Debug(debug bool) {} -// BeginAuth asks Facebook for an authentication end-point. func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return p.BeginAuthCtx(context.TODO(), state) +} + +// BeginAuthCtx asks Facebook for an authentication end-point. +func (p *Provider) BeginAuthCtx(ctx context.Context, state string) (goth.Session, error) { url := p.config.AuthCodeURL(state) session := &Session{ AuthURL: url, @@ -59,8 +63,13 @@ func (p *Provider) BeginAuth(state string) (goth.Session, error) { return session, nil } -// FetchUser will go to Facebook and access basic information about the user. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + return p.FetchUserCtx(context.TODO(), session) +} + +// FetchUserCtx will go to Facebook and access basic information about the user. +func (p *Provider) FetchUserCtx(ctx context.Context, session goth.Session) ( + goth.User, error) { sess := session.(*Session) user := goth.User{ AccessToken: sess.AccessToken, @@ -68,7 +77,12 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { ExpiresAt: sess.ExpiresAt, } - response, err := http.Get(endpointProfile + "&access_token=" + url.QueryEscape(sess.AccessToken)) + client, err := goth.HTTPClient(ctx) + if err != nil { + return user, err + } + + response, err := client.Get(endpointProfile + "&access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { return user, err } @@ -152,12 +166,16 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config { return c } -//RefreshToken refresh token is not provided by facebook func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return p.RefreshTokenCtx(context.TODO(), refreshToken) +} + +// RefreshTokenCtx refresh token is not provided by facebook +func (p *Provider) RefreshTokenCtx(ctx context.Context, refreshToken string) (*oauth2.Token, error) { return nil, errors.New("Refresh token is not provided by facebook") } -//RefreshTokenAvailable refresh token is not provided by facebook +// RefreshTokenAvailable refresh token is not provided by facebook func (p *Provider) RefreshTokenAvailable() bool { return false } diff --git a/providers/facebook/session.go b/providers/facebook/session.go index b0a651784..dad0696a1 100644 --- a/providers/facebook/session.go +++ b/providers/facebook/session.go @@ -3,10 +3,11 @@ package facebook import ( "encoding/json" "errors" - "github.com/markbates/goth" - "golang.org/x/oauth2" "strings" "time" + + "github.com/markbates/goth" + "golang.org/x/net/context" ) // Session stores data during the auth process with Facebook. @@ -24,10 +25,14 @@ func (s Session) GetAuthURL() (string, error) { return s.AuthURL, nil } -// Authorize the session with Facebook and return the access token to be stored for future use. func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + return s.AuthorizeCtx(context.TODO(), provider, params) +} + +// AuthorizeCtx the session with Facebook and return the access token to be stored for future use. +func (s *Session) AuthorizeCtx(ctx context.Context, provider goth.Provider, params goth.Params) (string, error) { p := provider.(*Provider) - token, err := p.config.Exchange(oauth2.NoContext, params.Get("code")) + token, err := p.config.Exchange(ctx, params.Get("code")) if err != nil { return "", err } diff --git a/providers/gplus/gplus.go b/providers/gplus/gplus.go index 4e1dbe0fa..35dc94c47 100644 --- a/providers/gplus/gplus.go +++ b/providers/gplus/gplus.go @@ -7,11 +7,11 @@ import ( "encoding/json" "io" "io/ioutil" - "net/http" "net/url" "strings" "github.com/markbates/goth" + "golang.org/x/net/context" "golang.org/x/oauth2" ) @@ -51,8 +51,12 @@ func (p *Provider) Name() string { // Debug is a no-op for the gplus package. func (p *Provider) Debug(debug bool) {} -// BeginAuth asks Google+ for an authentication end-point. func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return p.BeginAuthCtx(context.TODO(), state) +} + +// BeginAuthCtx asks Google+ for an authentication end-point. +func (p *Provider) BeginAuthCtx(ctx context.Context, state string) (goth.Session, error) { var opts []oauth2.AuthCodeOption if p.prompt != nil { opts = append(opts, p.prompt) @@ -64,8 +68,14 @@ func (p *Provider) BeginAuth(state string) (goth.Session, error) { return session, nil } -// FetchUser will go to Google+ and access basic information about the user. -func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { +func (p *Provider) FetchUser(session goth.Session) ( + goth.User, error) { + return p.FetchUserCtx(context.TODO(), session) +} + +// FetchUserCtx will go to Google+ and access basic information about the user. +func (p *Provider) FetchUserCtx(ctx context.Context, session goth.Session) ( + goth.User, error) { sess := session.(*Session) user := goth.User{ AccessToken: sess.AccessToken, @@ -74,7 +84,12 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { ExpiresAt: sess.ExpiresAt, } - response, err := http.Get(endpointProfile + "?access_token=" + url.QueryEscape(sess.AccessToken)) + client, err := goth.HTTPClient(ctx) + if err != nil { + return user, err + } + + response, err := client.Get(endpointProfile + "?access_token=" + url.QueryEscape(sess.AccessToken)) if err != nil { return user, err } @@ -145,15 +160,21 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config { return c } -//RefreshTokenAvailable refresh token is provided by auth provider or not +// RefreshTokenAvailable refresh token is provided by auth provider or not func (p *Provider) RefreshTokenAvailable() bool { return true } -//RefreshToken get new access token based on the refresh token -func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { +func (p *Provider) RefreshToken(refreshToken string) ( + *oauth2.Token, error) { + return p.RefreshTokenCtx(context.TODO(), refreshToken) +} + +// RefreshTokenCtx get new access token based on the refresh token +func (p *Provider) RefreshTokenCtx(ctx context.Context, refreshToken string) ( + *oauth2.Token, error) { token := &oauth2.Token{RefreshToken: refreshToken} - ts := p.config.TokenSource(oauth2.NoContext, token) + ts := p.config.TokenSource(ctx, token) newToken, err := ts.Token() if err != nil { return nil, err diff --git a/providers/gplus/session.go b/providers/gplus/session.go index c1372d6d7..fb4b9753b 100644 --- a/providers/gplus/session.go +++ b/providers/gplus/session.go @@ -3,10 +3,11 @@ package gplus import ( "encoding/json" "errors" - "github.com/markbates/goth" - "golang.org/x/oauth2" "strings" "time" + + "github.com/markbates/goth" + "golang.org/x/net/context" ) // Session stores data during the auth process with Facebook. @@ -25,10 +26,14 @@ func (s Session) GetAuthURL() (string, error) { return s.AuthURL, nil } -// Authorize the session with Google+ and return the access token to be stored for future use. func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + return s.AuthorizeCtx(context.TODO(), provider, params) +} + +// AuthorizeCtx the session with Google+ and return the access token to be stored for future use. +func (s *Session) AuthorizeCtx(ctx context.Context, provider goth.Provider, params goth.Params) (string, error) { p := provider.(*Provider) - token, err := p.config.Exchange(oauth2.NoContext, params.Get("code")) + token, err := p.config.Exchange(ctx, params.Get("code")) if err != nil { return "", err } diff --git a/providers/twitter/session.go b/providers/twitter/session.go index 8c205eed2..ad164a944 100644 --- a/providers/twitter/session.go +++ b/providers/twitter/session.go @@ -3,9 +3,11 @@ package twitter import ( "encoding/json" "errors" + "strings" + "github.com/markbates/goth" "github.com/mrjones/oauth" - "strings" + "golang.org/x/net/context" ) // Session stores data during the auth process with Twitter. @@ -23,10 +25,15 @@ func (s Session) GetAuthURL() (string, error) { return s.AuthURL, nil } -// Authorize the session with Twitter and return the access token to be stored for future use. func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + return s.AuthorizeCtx(context.TODO(), provider, params) +} + +// AuthorizeCtx the session with Twitter and return the access token to be stored for future use. +func (s *Session) AuthorizeCtx(ctx context.Context, provider goth.Provider, params goth.Params) (string, error) { p := provider.(*Provider) - accessToken, err := p.consumer.AuthorizeToken(s.RequestToken, params.Get("oauth_verifier")) + accessToken, err := p.consumer.AuthorizeTokenWithParamsCtx( + ctx, s.RequestToken, params.Get("oauth_verifier"), p.consumer.AdditionalParams) if err != nil { return "", err } diff --git a/providers/twitter/twitter.go b/providers/twitter/twitter.go index 587b32d94..42ccdd3f4 100644 --- a/providers/twitter/twitter.go +++ b/providers/twitter/twitter.go @@ -6,10 +6,15 @@ import ( "bytes" "encoding/json" "errors" + "io/ioutil" + "net/http" + "net/url" + "github.com/markbates/goth" "github.com/mrjones/oauth" + "golang.org/x/net/context" "golang.org/x/oauth2" - "io/ioutil" + "gopkg.in/webhelp.v1/whcompat" ) var ( @@ -32,6 +37,9 @@ func New(clientKey, secret, callbackURL string) *Provider { CallbackURL: callbackURL, } p.consumer = newConsumer(p, authorizeURL) + p.consumer.HttpClientFunc = func(ctx context.Context) (oauth.HttpClient, error) { + return goth.HTTPClient(ctx) + } return p } @@ -66,10 +74,14 @@ func (p *Provider) Debug(debug bool) { p.debug = debug } -// BeginAuth asks Twitter for an authentication end-point and a request token for a session. -// Twitter does not support the "state" variable. func (p *Provider) BeginAuth(state string) (goth.Session, error) { - requestToken, url, err := p.consumer.GetRequestTokenAndUrl(p.CallbackURL) + return p.BeginAuthCtx(context.TODO(), state) +} + +// BeginAuthCtx asks Twitter for an authentication end-point and a request token for a session. +// Twitter does not support the "state" variable. +func (p *Provider) BeginAuthCtx(ctx context.Context, state string) (goth.Session, error) { + requestToken, url, err := p.consumer.GetRequestTokenAndUrlWithParamsCtx(ctx, p.CallbackURL, p.consumer.AdditionalParams) session := &Session{ AuthURL: url, RequestToken: requestToken, @@ -77,17 +89,31 @@ func (p *Provider) BeginAuth(state string) (goth.Session, error) { return session, err } -// FetchUser will go to Twitter and access basic information about the user. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + return p.FetchUserCtx(context.TODO(), session) +} + +// FetchUserCtx will go to Twitter and access basic information about the user. +func (p *Provider) FetchUserCtx(ctx context.Context, session goth.Session) (goth.User, error) { user := goth.User{ Provider: p.Name(), } sess := session.(*Session) - response, err := p.consumer.Get( - endpointProfile, - map[string]string{"include_entities": "false", "skip_status": "true"}, - sess.AccessToken) + + client, err := p.consumer.MakeHttpClient(sess.AccessToken) + if err != nil { + return user, err + } + + req, err := http.NewRequest("GET", endpointProfile+"?"+(url.Values{ + "include_entities": []string{"false"}, + "skip_status": []string{"true"}}).Encode(), nil) + if err != nil { + return user, err + } + req = whcompat.WithContext(req, ctx) + response, err := client.Do(req) if err != nil { return user, err } @@ -124,8 +150,13 @@ func newConsumer(provider *Provider, authURL string) *oauth.Consumer { return c } -//RefreshToken refresh token is not provided by twitter func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return p.RefreshTokenCtx(context.TODO(), refreshToken) +} + +// RefreshTokenCtx refresh token is not provided by twitter +func (p *Provider) RefreshTokenCtx(ctx context.Context, refreshToken string) ( + *oauth2.Token, error) { return nil, errors.New("Refresh token is not provided by twitter") } diff --git a/session.go b/session.go index 2d40b50bb..9fa80fc96 100644 --- a/session.go +++ b/session.go @@ -1,5 +1,9 @@ package goth +import ( + "golang.org/x/net/context" +) + // Params is used to pass data to sessions for authorization. An existing // implementation, and the one most likely to be used, is `url.Values`. type Params interface { @@ -11,11 +15,18 @@ type Params interface { // the start and the end of the authorization process with a // 3rd party provider. type Session interface { - // GetAuthURL returns the URL for the authentication end-point for the provider. + // GetAuthURL returns the URL for the authentication end-point for the + // provider. Implementations should not make outbound HTTP requests. GetAuthURL() (string, error) - // Marshal generates a string representation of the Session for storing between requests. + // Marshal generates a string representation of the Session for storing + // between requests. Implementations should not make outbound HTTP requests. Marshal() string - // Authorize should validate the data from the provider and return an access token - // that can be stored for later access to the provider. + + // This method is deprecated. Please see AuthorizeCtx. Authorize(Provider, Params) (string, error) + + // This method is now preferred. + // Authorize should validate the data from the provider and return an access + // token that can be stored for later access to the provider. + AuthorizeCtx(context.Context, Provider, Params) (string, error) }