Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified providers/shopify/session.go
100755 → 100644
Empty file.
Empty file modified providers/shopify/session_test.go
100755 → 100644
Empty file.
Empty file modified providers/shopify/shopify.go
100755 → 100644
Empty file.
Empty file modified providers/shopify/shopify_test.go
100755 → 100644
Empty file.
27 changes: 21 additions & 6 deletions providers/twitterv2/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion providers/twitterv2/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
126 changes: 78 additions & 48 deletions providers/twitterv2/twitterv2.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
// 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,
Secret: secret,
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
}

Expand All @@ -59,7 +59,7 @@ type Provider struct {
CallbackURL string
HTTPClient *http.Client
debug bool
consumer *oauth.Consumer
config *oauth2.Config
providerName string
}

Expand All @@ -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
}
Expand All @@ -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
}
35 changes: 16 additions & 19 deletions providers/twitterv2/twitterv2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -39,25 +38,27 @@ 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) {
t.Parallel()
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)
Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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{
Expand All @@ -117,6 +115,5 @@ func init() {
})
ts := httptest.NewServer(p)

requestURL = ts.URL + "/oauth/request_token"
endpointProfile = ts.URL + "/2/users/me"
}
Loading