Skip to content

Commit

Permalink
Merge pull request #460 from joshcollie/twitter-api-v2
Browse files Browse the repository at this point in the history
Twitter API v2 support
  • Loading branch information
techknowlogick authored Jun 24, 2022
2 parents c7c7340 + 531478a commit 6fc8acb
Show file tree
Hide file tree
Showing 4 changed files with 397 additions and 0 deletions.
54 changes: 54 additions & 0 deletions providers/twitterv2/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package twitterv2

import (
"encoding/json"
"errors"
"strings"

"github.com/markbates/goth"
"github.com/mrjones/oauth"
)

// Session stores data during the auth process with Twitter.
type Session struct {
AuthURL string
AccessToken *oauth.AccessToken
RequestToken *oauth.RequestToken
}

// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Twitter provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
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) {
p := provider.(*Provider)
accessToken, err := p.consumer.AuthorizeToken(s.RequestToken, params.Get("oauth_verifier"))
if err != nil {
return "", err
}

s.AccessToken = accessToken
return accessToken.Token, err
}

// Marshal the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}

func (s Session) String() string {
return s.Marshal()
}

// UnmarshalSession will unmarshal a JSON string into a session.
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}
48 changes: 48 additions & 0 deletions providers/twitterv2/session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package twitterv2_test

import (
"testing"

"github.com/markbates/goth"
"github.com/markbates/goth/providers/twitterv2"
"github.com/stretchr/testify/assert"
)

func Test_Implements_Session(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &twitterv2.Session{}

a.Implements((*goth.Session)(nil), s)
}

func Test_GetAuthURL(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &twitterv2.Session{}

_, err := s.GetAuthURL()
a.Error(err)

s.AuthURL = "/foo"

url, _ := s.GetAuthURL()
a.Equal(url, "/foo")
}

func Test_ToJSON(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &twitterv2.Session{}

data := s.Marshal()
a.Equal(data, `{"AuthURL":"","AccessToken":null,"RequestToken":null}`)
}

func Test_String(t *testing.T) {
t.Parallel()
a := assert.New(t)
s := &twitterv2.Session{}

a.Equal(s.String(), s.Marshal())
}
173 changes: 173 additions & 0 deletions providers/twitterv2/twitterv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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"
"io/ioutil"
"net/http"

"fmt"

"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"
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)
return p
}

// NewAuthenticate is the almost same as New.
// NewAuthenticate uses the authenticate URL instead of the authorize URL.
func NewAuthenticate(clientKey, secret, callbackURL string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "twitterv2",
}
p.consumer = newConsumer(p, authenticateURL)
return p
}

// Provider is the implementation of `goth.Provider` for accessing Twitter.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
debug bool
consumer *oauth.Consumer
providerName string
}

// Name is the name used to retrieve this provider later.
func (p *Provider) Name() string {
return p.providerName
}

// SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
func (p *Provider) SetName(name string) {
p.providerName = name
}

func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}

// Debug sets the logging of the OAuth client to verbose.
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)
session := &Session{
AuthURL: url,
RequestToken: requestToken,
}
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) {
sess := session.(*Session)
user := goth.User{
Provider: p.Name(),
}

if sess.AccessToken == nil {
// 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,
nil,
sess.AccessToken)
if err != nil {
return user, err
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
}

bits, err := ioutil.ReadAll(response.Body)
if err != nil {
return user, err
}

userInfo := struct {
Data map[string]interface{} `json:"data"`
}{}

err = json.NewDecoder(bytes.NewReader(bits)).Decode(&userInfo)
if err != nil {
return user, err
}

user.RawData = userInfo.Data
user.Name = user.RawData["name"].(string)
user.NickName = user.RawData["username"].(string)
if user.RawData["email"] != nil {
user.Email = user.RawData["email"].(string)
}
user.Description = user.RawData["description"].(string)
user.AvatarURL = user.RawData["profile_image_url"].(string)
user.UserID = user.RawData["id"].(string)
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)
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 not provided by twitter
func (p *Provider) RefreshTokenAvailable() bool {
return false
}
Loading

0 comments on commit 6fc8acb

Please sign in to comment.