diff --git a/providers/shopify/session.go b/providers/shopify/session.go old mode 100755 new mode 100644 diff --git a/providers/shopify/session_test.go b/providers/shopify/session_test.go old mode 100755 new mode 100644 diff --git a/providers/shopify/shopify.go b/providers/shopify/shopify.go old mode 100755 new mode 100644 diff --git a/providers/shopify/shopify_test.go b/providers/shopify/shopify_test.go old mode 100755 new mode 100644 diff --git a/providers/twitterv2/session.go b/providers/twitterv2/session.go index ef298dde7..47fa4e384 100644 --- a/providers/twitterv2/session.go +++ b/providers/twitterv2/session.go @@ -4,16 +4,19 @@ import ( "encoding/json" "errors" "strings" + "time" "github.com/markbates/goth" - "github.com/mrjones/oauth" + "golang.org/x/oauth2" ) // Session stores data during the auth process with Twitter. type Session struct { AuthURL string - AccessToken *oauth.AccessToken - RequestToken *oauth.RequestToken + AccessToken string + RefreshToken string + ExpiresAt time.Time + CodeVerifier string } // GetAuthURL will return the URL set by calling the `BeginAuth` function on the Twitter provider. @@ -27,13 +30,25 @@ func (s Session) GetAuthURL() (string, error) { // 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) { p := provider.(*Provider) - accessToken, err := p.consumer.AuthorizeToken(s.RequestToken, params.Get("oauth_verifier")) + + opts := []oauth2.AuthCodeOption{} + if s.CodeVerifier != "" { + opts = append(opts, oauth2.VerifierOption(s.CodeVerifier)) + } + + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code"), opts...) if err != nil { return "", err } - s.AccessToken = accessToken - return accessToken.Token, err + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + return token.AccessToken, err } // Marshal the session into a string diff --git a/providers/twitterv2/session_test.go b/providers/twitterv2/session_test.go index 9ef101a5c..1794503a1 100644 --- a/providers/twitterv2/session_test.go +++ b/providers/twitterv2/session_test.go @@ -36,7 +36,7 @@ func Test_ToJSON(t *testing.T) { s := &twitterv2.Session{} data := s.Marshal() - a.Equal(data, `{"AuthURL":"","AccessToken":null,"RequestToken":null}`) + a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z","CodeVerifier":""}`) } func Test_String(t *testing.T) { diff --git a/providers/twitterv2/twitterv2.go b/providers/twitterv2/twitterv2.go index ee04ca72a..3718b786e 100644 --- a/providers/twitterv2/twitterv2.go +++ b/providers/twitterv2/twitterv2.go @@ -1,33 +1,25 @@ -// Package twitterv2 implements the OAuth protocol for authenticating users through Twitter. -// This package can be used as a reference implementation of an OAuth provider for Goth. package twitterv2 import ( "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" "github.com/markbates/goth" - "github.com/mrjones/oauth" "golang.org/x/oauth2" ) var ( - requestURL = "https://api.twitter.com/oauth/request_token" - authorizeURL = "https://api.twitter.com/oauth/authorize" - authenticateURL = "https://api.twitter.com/oauth/authenticate" - tokenURL = "https://api.twitter.com/oauth/access_token" + AuthURL = "https://twitter.com/i/oauth2/authorize" + TokenURL = "https://api.twitter.com/2/oauth2/token" endpointProfile = "https://api.twitter.com/2/users/me" ) // New creates a new Twitter provider, and sets up important connection details. // You should always call `twitter.New` to get a new Provider. Never try to create // one manually. -// -// If you'd like to use authenticate instead of authorize, use NewAuthenticate instead. func New(clientKey, secret, callbackURL string) *Provider { p := &Provider{ ClientKey: clientKey, @@ -35,20 +27,28 @@ func New(clientKey, secret, callbackURL string) *Provider { CallbackURL: callbackURL, providerName: "twitterv2", } - p.consumer = newConsumer(p, authorizeURL) + p.config = newConfig(p, []string{"users.read", "tweet.read", "offline.access"}) return p } -// NewAuthenticate is the almost same as New. -// NewAuthenticate uses the authenticate URL instead of the authorize URL. +// NewAuthenticate is the same as New for OAuth 2.0. +// Kept for backward compatibility. func NewAuthenticate(clientKey, secret, callbackURL string) *Provider { + return New(clientKey, secret, callbackURL) +} + +// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to +func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider { p := &Provider{ ClientKey: clientKey, Secret: secret, CallbackURL: callbackURL, providerName: "twitterv2", } - p.consumer = newConsumer(p, authenticateURL) + AuthURL = authURL + TokenURL = tokenURL + endpointProfile = profileURL + p.config = newConfig(p, scopes) return p } @@ -59,7 +59,7 @@ type Provider struct { CallbackURL string HTTPClient *http.Client debug bool - consumer *oauth.Consumer + config *oauth2.Config providerName string } @@ -83,32 +83,47 @@ func (p *Provider) Debug(debug bool) { } // BeginAuth asks Twitter for an authentication end-point and a request token for a session. -// Twitter does not support the "state" variable. +// Twitter uses PKCE for OAuth 2.0. func (p *Provider) BeginAuth(state string) (goth.Session, error) { - requestToken, url, err := p.consumer.GetRequestTokenAndUrl(p.CallbackURL) + verifier := oauth2.GenerateVerifier() + + url := p.config.AuthCodeURL( + state, + oauth2.S256ChallengeOption(verifier), + ) session := &Session{ AuthURL: url, - RequestToken: requestToken, + CodeVerifier: verifier, } - return session, err + return session, nil } // FetchUser will go to Twitter and access basic information about the user. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { sess := session.(*Session) user := goth.User{ - Provider: p.Name(), + Provider: p.Name(), + AccessToken: sess.AccessToken, + RefreshToken: sess.RefreshToken, + ExpiresAt: sess.ExpiresAt, } - if sess.AccessToken == nil { + if sess.AccessToken == "" { // data is not yet retrieved since accessToken is still empty return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) } - response, err := p.consumer.Get( - endpointProfile, - map[string]string{"user.fields": "id,name,username,description,profile_image_url,location"}, - sess.AccessToken) + req, err := http.NewRequest("GET", endpointProfile, nil) + if err != nil { + return user, err + } + + q := req.URL.Query() + q.Add("user.fields", "id,name,username,description,profile_image_url,location") + req.URL.RawQuery = q.Encode() + + req.Header.Add("Authorization", "Bearer "+sess.AccessToken) + response, err := p.Client().Do(req) if err != nil { return user, err } @@ -133,41 +148,56 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { } user.RawData = userInfo.Data - user.Name = user.RawData["name"].(string) - user.NickName = user.RawData["username"].(string) + if user.RawData["name"] != nil { + user.Name = user.RawData["name"].(string) + } + if user.RawData["username"] != nil { + user.NickName = user.RawData["username"].(string) + } if user.RawData["description"] != nil { user.Description = user.RawData["description"].(string) } - user.AvatarURL = user.RawData["profile_image_url"].(string) - user.UserID = user.RawData["id"].(string) + if user.RawData["profile_image_url"] != nil { + user.AvatarURL = user.RawData["profile_image_url"].(string) + } + if user.RawData["id"] != nil { + user.UserID = user.RawData["id"].(string) + } if user.RawData["location"] != nil { user.Location = user.RawData["location"].(string) } - user.AccessToken = sess.AccessToken.Token - user.AccessTokenSecret = sess.AccessToken.Secret + return user, err } -func newConsumer(provider *Provider, authURL string) *oauth.Consumer { - c := oauth.NewConsumer( - provider.ClientKey, - provider.Secret, - oauth.ServiceProvider{ - RequestTokenUrl: requestURL, - AuthorizeTokenUrl: authURL, - AccessTokenUrl: tokenURL, - }) - - c.Debug(provider.debug) +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: AuthURL, + TokenURL: TokenURL, + AuthStyle: oauth2.AuthStyleInHeader, + }, + Scopes: scopes, + } + return c } -// RefreshToken refresh token is not provided by twitter -func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { - return nil, errors.New("Refresh token is not provided by twitter") +// RefreshTokenAvailable refresh token is provided by twitter +func (p *Provider) RefreshTokenAvailable() bool { + return true } -// RefreshTokenAvailable refresh token is not provided by twitter -func (p *Provider) RefreshTokenAvailable() bool { - return false +// RefreshToken get a new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err } diff --git a/providers/twitterv2/twitterv2_test.go b/providers/twitterv2/twitterv2_test.go index c7649aafa..51507729d 100644 --- a/providers/twitterv2/twitterv2_test.go +++ b/providers/twitterv2/twitterv2_test.go @@ -2,15 +2,14 @@ package twitterv2 import ( "encoding/json" - "fmt" "net/http" "net/http/httptest" "os" "testing" + "time" "github.com/gorilla/pat" "github.com/markbates/goth" - "github.com/mrjones/oauth" "github.com/stretchr/testify/assert" ) @@ -39,17 +38,19 @@ func Test_BeginAuth(t *testing.T) { session, err := provider.BeginAuth("state") s := session.(*Session) a.NoError(err) - a.Contains(s.AuthURL, "authorize?oauth_token=TOKEN") - a.Equal("TOKEN", s.RequestToken.Token) - a.Equal("SECRET", s.RequestToken.Secret) + a.Contains(s.AuthURL, "twitter.com/i/oauth2/authorize") + a.Contains(s.AuthURL, "code_challenge=") + a.Contains(s.AuthURL, "code_challenge_method=S256") + a.NotEmpty(s.CodeVerifier) provider = twitterProviderAuthenticate() session, err = provider.BeginAuth("state") s = session.(*Session) a.NoError(err) - a.Contains(s.AuthURL, "authenticate?oauth_token=TOKEN") - a.Equal("TOKEN", s.RequestToken.Token) - a.Equal("SECRET", s.RequestToken.Secret) + a.Contains(s.AuthURL, "twitter.com/i/oauth2/authorize") + a.Contains(s.AuthURL, "code_challenge=") + a.Contains(s.AuthURL, "code_challenge_method=S256") + a.NotEmpty(s.CodeVerifier) } func Test_FetchUser(t *testing.T) { @@ -57,7 +58,7 @@ func Test_FetchUser(t *testing.T) { a := assert.New(t) provider := twitterProvider() - session := Session{AccessToken: &oauth.AccessToken{Token: "TOKEN", Secret: "SECRET"}} + session := Session{AccessToken: "TOKEN", RefreshToken: "REFRESH", ExpiresAt: time.Now()} user, err := provider.FetchUser(&session) a.NoError(err) @@ -69,7 +70,8 @@ func Test_FetchUser(t *testing.T) { a.Equal("1234", user.UserID) a.Equal("Springfield", user.Location) a.Equal("TOKEN", user.AccessToken) - a.Equal("", user.Email) + a.Equal("REFRESH", user.RefreshToken) + a.Equal("", user.Email) // email is not strictly mapped right now natively } func Test_SessionFromJSON(t *testing.T) { @@ -78,14 +80,13 @@ func Test_SessionFromJSON(t *testing.T) { provider := twitterProvider() - s, err := provider.UnmarshalSession(`{"AuthURL":"http://com/auth_url","AccessToken":{"Token":"1234567890","Secret":"secret!!","AdditionalData":{}},"RequestToken":{"Token":"0987654321","Secret":"!!secret"}}`) + s, err := provider.UnmarshalSession(`{"AuthURL":"http://com/auth_url","AccessToken":"1234567890","RefreshToken":"refresh","CodeVerifier":"verifier"}`) a.NoError(err) session := s.(*Session) a.Equal(session.AuthURL, "http://com/auth_url") - a.Equal(session.AccessToken.Token, "1234567890") - a.Equal(session.AccessToken.Secret, "secret!!") - a.Equal(session.RequestToken.Token, "0987654321") - a.Equal(session.RequestToken.Secret, "!!secret") + a.Equal(session.AccessToken, "1234567890") + a.Equal(session.RefreshToken, "refresh") + a.Equal(session.CodeVerifier, "verifier") } func twitterProvider() *Provider { @@ -98,9 +99,6 @@ func twitterProviderAuthenticate() *Provider { func init() { p := pat.New() - p.Get("/oauth/request_token", func(res http.ResponseWriter, req *http.Request) { - fmt.Fprint(res, "oauth_token=TOKEN&oauth_token_secret=SECRET") - }) p.Get("/2/users/me", func(res http.ResponseWriter, req *http.Request) { data := map[string]interface{}{ "data": map[string]string{ @@ -117,6 +115,5 @@ func init() { }) ts := httptest.NewServer(p) - requestURL = ts.URL + "/oauth/request_token" endpointProfile = ts.URL + "/2/users/me" }