From 644067ff37b416ee66330b1e1d049ade353123f7 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:17:06 +0000 Subject: [PATCH 01/17] Patreon provider --- providers/patreon/patreon.go | 186 ++++++++++++++++++++++++++++++ providers/patreon/patreon_test.go | 53 +++++++++ providers/patreon/session.go | 63 ++++++++++ providers/patreon/session_test.go | 37 ++++++ 4 files changed, 339 insertions(+) create mode 100644 providers/patreon/patreon.go create mode 100644 providers/patreon/patreon_test.go create mode 100644 providers/patreon/session.go create mode 100644 providers/patreon/session_test.go diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go new file mode 100644 index 000000000..92cd5e8e3 --- /dev/null +++ b/providers/patreon/patreon.go @@ -0,0 +1,186 @@ +// Package patreon implements the OAuth protocol for authenticating users through Patreon. +package patreon + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +//goland:noinspection GoUnusedConst +const ( + // ScopeIdentity provides read access to data about the user. See the /identity endpoint documentation for details about what data is available. + ScopeIdentity = "identity" + + // ScopeIdentityEmail provides read access to the user’s email. + ScopeIdentityEmail = "identity[email]" + + // ScopeIdentityMemberships provides read access to the user’s memberships. + ScopeIdentityMemberships = "identity.memberships" + + // ScopeCampaigns provides read access to basic campaign data. See the /campaign endpoint documentation for details about what data is available. + ScopeCampaigns = "campaigns" + + // ScopeCampaignsWebhook provides read, write, update, and delete access to the campaign’s webhooks created by the client. + ScopeCampaignsWebhook = "w:campaigns.webhook" + + // ScopeCampaignsMembers provides read access to data about a campaign’s members. See the /members endpoint documentation for details about what data is available. Also allows the same information to be sent via webhooks created by your client. + ScopeCampaignsMembers = "campaigns.members" + + // ScopeCampaignsMembersEmail provides read access to the member’s email. Also allows the same information to be sent via webhooks created by your client. + ScopeCampaignsMembersEmail = "campaigns.members[email]" + + // ScopeCampaignsMembersAddress provides read access to the member’s address, if an address was collected in the pledge flow. Also allows the same information to be sent via webhooks created by your client. + ScopeCampaignsMembersAddress = "campaigns.members.address" + + // ScopeCampaignsPosts provides read access to the posts on a campaign. + ScopeCampaignsPosts = "campaigns.posts" +) + +// New creates a new Patreon provider and sets up important connection details. +// You should always call `Patreon.New` to get a new Provider. Never try to +// create one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "patreon", + } + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing Patreon. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string +} + +// Name gets the name used to retrieve this provider. +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 is a no-op for the Patreon package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Patreon for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + url := p.config.AuthCodeURL(state) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +// FetchUser will go to Patreon and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + s := session.(*Session) + user := goth.User{ + AccessToken: s.AccessToken, + Provider: p.Name(), + RefreshToken: s.RefreshToken, + ExpiresAt: s.ExpiresAt, + } + + if user.AccessToken == "" { + // data is not yet retrieved since accessToken is still empty + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", "https://www.patreon.com/api/oauth2/v2/identity", nil) + if err != nil { + return user, err + } + req.Header.Set("Authorization", "Bearer "+s.AccessToken) + resp, err := p.Client().Do(req) + if err != nil { + return user, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) + } + + u := User{} + err = json.NewDecoder(resp.Body).Decode(&u) + if err != nil { + return user, err + } + + user.Name = u.Data.Attributes.FullName + user.Email = u.Data.Attributes.Email + user.UserID = u.Data.ID + + return user, err +} + +func newConfig(p *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: p.ClientKey, + ClientSecret: p.Secret, + RedirectURL: p.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://www.patreon.com/oauth2/authorize", + TokenURL: "https://www.patreon.com/api/oauth2/token", + }, + Scopes: []string{ScopeIdentity, ScopeIdentityEmail}, + } + + defaultScopes := map[string]struct{}{ + ScopeIdentity: {}, + ScopeIdentityEmail: {}, + } + + for _, scope := range scopes { + if _, exists := defaultScopes[scope]; !exists { + c.Scopes = append(c.Scopes, scope) + } + } + + return c +} + +// 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) { + 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 +} + +type User struct { + Data struct { + Attributes struct { + Email string `json:"email"` + FullName string `json:"full_name"` + } `json:"attributes"` + ID string `json:"id"` + } `json:"data"` +} diff --git a/providers/patreon/patreon_test.go b/providers/patreon/patreon_test.go new file mode 100644 index 000000000..a2ec13d3b --- /dev/null +++ b/providers/patreon/patreon_test.go @@ -0,0 +1,53 @@ +package patreon + +import ( + "os" + "testing" + + "github.com/markbates/goth" + "github.com/stretchr/testify/assert" +) + +func provider() *Provider { + return New(os.Getenv("PATREON_KEY"), os.Getenv("PATREON_SECRET"), "/foo", "user") +} + +func Test_New(t *testing.T) { + t.Parallel() + a := assert.New(t) + p := provider() + + a.Equal(p.ClientKey, os.Getenv("PATREON_KEY")) + a.Equal(p.Secret, os.Getenv("PATREON_SECRET")) + a.Equal(p.CallbackURL, "/foo") +} + +func Test_ImplementsProvider(t *testing.T) { + t.Parallel() + a := assert.New(t) + a.Implements((*goth.Provider)(nil), provider()) +} + +func Test_BeginAuth(t *testing.T) { + t.Parallel() + a := assert.New(t) + + p := provider() + session, err := p.BeginAuth("test_state") + s := session.(*Session) + a.NoError(err) + a.Contains(s.AuthURL, "www.patreon.com/oauth2/authorize") +} + +func Test_SessionFromJSON(t *testing.T) { + t.Parallel() + a := assert.New(t) + + p := provider() + session, err := p.UnmarshalSession(`{"AuthURL":"http://www.patreon.com/oauth2/authorize","AccessToken":"1234567890"}`) + a.NoError(err) + + s := session.(*Session) + a.Equal(s.AuthURL, "http://www.patreon.com/oauth2/authorize") + a.Equal(s.AccessToken, "1234567890") +} diff --git a/providers/patreon/session.go b/providers/patreon/session.go new file mode 100644 index 000000000..7e5f22f03 --- /dev/null +++ b/providers/patreon/session.go @@ -0,0 +1,63 @@ +package patreon + +import ( + "encoding/json" + "errors" + "time" + + "github.com/markbates/goth" +) + +// Session stores data during the auth process with Patreon. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the +// Patreon provider. +func (s *Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize completes the authorization with Patreon and returns the access +// token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", 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 marshals a session into a JSON string. +func (s *Session) Marshal() string { + j, _ := json.Marshal(s) + return string(j) +} + +// String is equivalent to Marshal. It returns a JSON representation of the session. +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) { + s := Session{} + err := json.Unmarshal([]byte(data), &s) + return &s, err +} diff --git a/providers/patreon/session_test.go b/providers/patreon/session_test.go new file mode 100644 index 000000000..7b2e7a4e9 --- /dev/null +++ b/providers/patreon/session_test.go @@ -0,0 +1,37 @@ +package patreon + +import ( + "testing" + + "github.com/markbates/goth" + "github.com/stretchr/testify/assert" +) + +func Test_ImplementsSession(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &Session{} + a.Implements((*goth.Session)(nil), s) +} + +func Test_GetAuthURL(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &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 := &Session{} + + data := s.Marshal() + a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`) +} From 7998320d87e4c4e51d77288cf4b1d1e858ec6074 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:24:02 +0000 Subject: [PATCH 02/17] Update example --- examples/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/main.go b/examples/main.go index 16bdd8af3..84593c710 100644 --- a/examples/main.go +++ b/examples/main.go @@ -46,6 +46,7 @@ import ( "github.com/markbates/goth/providers/okta" "github.com/markbates/goth/providers/onedrive" "github.com/markbates/goth/providers/openidConnect" + "github.com/markbates/goth/providers/patreon" "github.com/markbates/goth/providers/paypal" "github.com/markbates/goth/providers/salesforce" "github.com/markbates/goth/providers/seatalk" @@ -147,6 +148,7 @@ func main() { mastodon.New(os.Getenv("MASTODON_KEY"), os.Getenv("MASTODON_SECRET"), "http://localhost:3000/auth/mastodon/callback", "read:accounts"), wecom.New(os.Getenv("WECOM_CORP_ID"), os.Getenv("WECOM_SECRET"), os.Getenv("WECOM_AGENT_ID"), "http://localhost:3000/auth/wecom/callback"), zoom.New(os.Getenv("ZOOM_KEY"), os.Getenv("ZOOM_SECRET"), "http://localhost:3000/auth/zoom/callback", "read:user"), + patreon.New(os.Getenv("PATREON_KEY"), os.Getenv("PATREON_SECRET"), "http://localhost:3000/auth/patreon/callback"), ) // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) @@ -217,6 +219,7 @@ func main() { m["mastodon"] = "Mastodon" m["wecom"] = "WeCom" m["zoom"] = "Zoom" + m["patreon"] = "Patreon" var keys []string for k := range m { From 4a26e1c7f176c2a57afd2a96484ccf8769c18ea0 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:34:16 +0000 Subject: [PATCH 03/17] Use consts --- providers/patreon/patreon.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 92cd5e8e3..6b598dd4b 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -10,6 +10,18 @@ import ( "golang.org/x/oauth2" ) +const ( + // AuthorizationURL specifies Patreon's OAuth2 authorization endpoint (see https://tools.ietf.org/html/rfc6749#section-3.1). + // See Example_refreshToken for examples. + authorizationURL = "https://www.patreon.com/oauth2/authorize" + + // AccessTokenURL specifies Patreon's OAuth2 token endpoint (see https://tools.ietf.org/html/rfc6749#section-3.2). + // See Example_refreshToken for examples. + accessTokenURL = "https://www.patreon.com/api/oauth2/token" + + profileURL = "https://www.patreon.com/api/oauth2/v2/identity" +) + //goland:noinspection GoUnusedConst const ( // ScopeIdentity provides read access to data about the user. See the /identity endpoint documentation for details about what data is available. @@ -105,7 +117,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) } - req, err := http.NewRequest("GET", "https://www.patreon.com/api/oauth2/v2/identity", nil) + req, err := http.NewRequest("GET", profileURL, nil) if err != nil { return user, err } @@ -139,8 +151,8 @@ func newConfig(p *Provider, scopes []string) *oauth2.Config { ClientSecret: p.Secret, RedirectURL: p.CallbackURL, Endpoint: oauth2.Endpoint{ - AuthURL: "https://www.patreon.com/oauth2/authorize", - TokenURL: "https://www.patreon.com/api/oauth2/token", + AuthURL: authorizationURL, + TokenURL: accessTokenURL, }, Scopes: []string{ScopeIdentity, ScopeIdentityEmail}, } From 76e7a50bfdbe45207b30fd835b78e5ad948fd714 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:36:01 +0000 Subject: [PATCH 04/17] Sort list --- examples/main.go | 60 ++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/examples/main.go b/examples/main.go index 84593c710..75946dede 100644 --- a/examples/main.go +++ b/examples/main.go @@ -161,6 +161,10 @@ func main() { m := make(map[string]string) m["amazon"] = "Amazon" + m["apple"] = "Apple" + m["auth0"] = "Auth0" + m["azuread"] = "Azure AD" + m["battlenet"] = "Battlenet" m["bitbucket"] = "Bitbucket" m["box"] = "Box" m["dailymotion"] = "Dailymotion" @@ -176,50 +180,46 @@ func main() { m["gitlab"] = "Gitlab" m["google"] = "Google" m["gplus"] = "Google Plus" - m["shopify"] = "Shopify" - m["soundcloud"] = "SoundCloud" - m["spotify"] = "Spotify" - m["steam"] = "Steam" - m["stripe"] = "Stripe" - m["tiktok"] = "TikTok" - m["twitch"] = "Twitch" - m["uber"] = "Uber" - m["wepay"] = "Wepay" - m["yahoo"] = "Yahoo" - m["yammer"] = "Yammer" m["heroku"] = "Heroku" m["instagram"] = "Instagram" m["intercom"] = "Intercom" m["kakao"] = "Kakao" m["lastfm"] = "Last FM" - m["linkedin"] = "Linkedin" m["line"] = "LINE" - m["onedrive"] = "Onedrive" - m["azuread"] = "Azure AD" + m["linkedin"] = "Linkedin" + m["mastodon"] = "Mastodon" + m["meetup"] = "Meetup.com" m["microsoftonline"] = "Microsoft Online" - m["battlenet"] = "Battlenet" + m["naver"] = "Naver" + m["nextcloud"] = "NextCloud" + m["okta"] = "Okta" + m["onedrive"] = "Onedrive" + m["openid-connect"] = "OpenID Connect" + m["patreon"] = "Patreon" m["paypal"] = "Paypal" + m["salesforce"] = "Salesforce" + m["seatalk"] = "SeaTalk" + m["shopify"] = "Shopify" + m["slack"] = "Slack" + m["soundcloud"] = "SoundCloud" + m["spotify"] = "Spotify" + m["steam"] = "Steam" + m["strava"] = "Strava" + m["stripe"] = "Stripe" + m["tiktok"] = "TikTok" + m["twitch"] = "Twitch" m["twitter"] = "Twitter" m["twitterv2"] = "Twitter" - m["salesforce"] = "Salesforce" m["typetalk"] = "Typetalk" - m["slack"] = "Slack" - m["meetup"] = "Meetup.com" - m["auth0"] = "Auth0" - m["openid-connect"] = "OpenID Connect" - m["xero"] = "Xero" + m["uber"] = "Uber" m["vk"] = "VK" - m["naver"] = "Naver" - m["yandex"] = "Yandex" - m["nextcloud"] = "NextCloud" - m["seatalk"] = "SeaTalk" - m["apple"] = "Apple" - m["strava"] = "Strava" - m["okta"] = "Okta" - m["mastodon"] = "Mastodon" m["wecom"] = "WeCom" + m["wepay"] = "Wepay" + m["xero"] = "Xero" + m["yahoo"] = "Yahoo" + m["yammer"] = "Yammer" + m["yandex"] = "Yandex" m["zoom"] = "Zoom" - m["patreon"] = "Patreon" var keys []string for k := range m { From c23c9e3d102f5dc13022190b27beecaefe012b8b Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:47:36 +0000 Subject: [PATCH 05/17] Scopes len check --- providers/patreon/patreon.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 6b598dd4b..08defc849 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -162,12 +162,11 @@ func newConfig(p *Provider, scopes []string) *oauth2.Config { ScopeIdentityEmail: {}, } - for _, scope := range scopes { - if _, exists := defaultScopes[scope]; !exists { + if len(scopes) > 0 { + for _, scope := range scopes { c.Scopes = append(c.Scopes, scope) } } - return c } From 07019d8fc73147cd55003a073cf81a0509f6d705 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:48:07 +0000 Subject: [PATCH 06/17] Remove default scopes --- providers/patreon/patreon.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 08defc849..5862c518b 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -145,21 +145,16 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, err } -func newConfig(p *Provider, scopes []string) *oauth2.Config { +func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { c := &oauth2.Config{ - ClientID: p.ClientKey, - ClientSecret: p.Secret, - RedirectURL: p.CallbackURL, + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, Endpoint: oauth2.Endpoint{ - AuthURL: authorizationURL, - TokenURL: accessTokenURL, + AuthURL: authURL, + TokenURL: tokenURL, }, - Scopes: []string{ScopeIdentity, ScopeIdentityEmail}, - } - - defaultScopes := map[string]struct{}{ - ScopeIdentity: {}, - ScopeIdentityEmail: {}, + Scopes: []string{}, } if len(scopes) > 0 { From 83f6f1891f91f59f213abe35ffc868845fdd4dc6 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:53:01 +0000 Subject: [PATCH 07/17] Fix type --- providers/patreon/patreon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 5862c518b..9644b42d1 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -53,7 +53,7 @@ const ( ) // New creates a new Patreon provider and sets up important connection details. -// You should always call `Patreon.New` to get a new Provider. Never try to +// You should always call `patreon.New` to get a new provider. Never try to // create one manually. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { p := &Provider{ From bcec8241e9676543860218ea3af76ae902f29fa7 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:53:59 +0000 Subject: [PATCH 08/17] NewCustomisedURL --- providers/patreon/patreon.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 9644b42d1..868e8abf2 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -56,13 +56,19 @@ const ( // You should always call `patreon.New` to get a new provider. Never try to // create one manually. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + return NewCustomisedURL(clientKey, secret, callbackURL, authorizationURL, accessTokenURL, profileURL, scopes...) +} + +// 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: "patreon", + profileURL: profileURL, } - p.config = newConfig(p, scopes) + p.config = newConfig(p, authURL, tokenURL, scopes) return p } From 1b75cb16d43280cf64d9ae63e3974b4df223b108 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:57:00 +0000 Subject: [PATCH 09/17] Update FetchUser --- providers/patreon/patreon.go | 68 +++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 868e8abf2..495b1ad5e 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -110,12 +110,12 @@ func (p *Provider) BeginAuth(state string) (goth.Session, error) { // FetchUser will go to Patreon and access basic information about the user. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { - s := session.(*Session) + sesh := session.(*Session) user := goth.User{ - AccessToken: s.AccessToken, + AccessToken: sesh.AccessToken, Provider: p.Name(), - RefreshToken: s.RefreshToken, - ExpiresAt: s.ExpiresAt, + RefreshToken: sesh.RefreshToken, + ExpiresAt: sesh.ExpiresAt, } if user.AccessToken == "" { @@ -123,30 +123,33 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) } - req, err := http.NewRequest("GET", profileURL, nil) + req, err := http.NewRequest("GET", p.profileURL, nil) if err != nil { return user, err } - req.Header.Set("Authorization", "Bearer "+s.AccessToken) - resp, err := p.Client().Do(req) + + req.Header.Add("authorization", "Bearer "+sesh.AccessToken) + response, err := p.Client().Do(req) if err != nil { return user, err } - defer resp.Body.Close() + defer response.Body.Close() - if resp.StatusCode != http.StatusOK { - return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) } - u := User{} - err = json.NewDecoder(resp.Body).Decode(&u) + bits, err := io.ReadAll(response.Body) if err != nil { return user, err } - user.Name = u.Data.Attributes.FullName - user.Email = u.Data.Attributes.Email - user.UserID = u.Data.ID + err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) + if err != nil { + return user, err + } + + err = userFromReader(bytes.NewReader(bits), &user) return user, err } @@ -171,6 +174,31 @@ func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *o return c } +func userFromReader(r io.Reader, user *goth.User) error { + u := struct { + Data struct { + Attributes struct { + Created time.Time `json:"created"` + Email string `json:"email"` + FullName string `json:"full_name"` + ImageURL string `json:"image_url"` + Vanity string `json:"vanity"` + } `json:"attributes"` + ID string `json:"id"` + } `json:"data"` + }{} + err := json.NewDecoder(r).Decode(&u) + if err != nil { + return err + } + user.Email = u.Data.Attributes.Email + user.Name = u.Data.Attributes.FullName + user.NickName = u.Data.Attributes.Vanity + user.UserID = u.Data.ID + user.AvatarURL = u.Data.Attributes.ImageURL + return nil +} + // RefreshTokenAvailable refresh token is provided by auth provider or not func (p *Provider) RefreshTokenAvailable() bool { return true @@ -186,13 +214,3 @@ func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { } return newToken, err } - -type User struct { - Data struct { - Attributes struct { - Email string `json:"email"` - FullName string `json:"full_name"` - } `json:"attributes"` - ID string `json:"id"` - } `json:"data"` -} From 8e973660f344df610036bc57b7c74639a0a80fbf Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:57:40 +0000 Subject: [PATCH 10/17] Comments --- providers/patreon/patreon.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 495b1ad5e..db652c5a9 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -1,4 +1,3 @@ -// Package patreon implements the OAuth protocol for authenticating users through Patreon. package patreon import ( @@ -82,7 +81,7 @@ type Provider struct { providerName string } -// Name gets the name used to retrieve this provider. +// Name gets the name used to retrieve this provider later. func (p *Provider) Name() string { return p.providerName } From 803328e09a26e5b9be6829128697a6efab10d8ff Mon Sep 17 00:00:00 2001 From: James Eagle Date: Tue, 1 Nov 2022 21:59:23 +0000 Subject: [PATCH 11/17] Tidy --- providers/patreon/patreon.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index db652c5a9..4413bb994 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -1,9 +1,12 @@ package patreon import ( + "bytes" "encoding/json" "fmt" + "io" "net/http" + "time" "github.com/markbates/goth" "golang.org/x/oauth2" @@ -79,6 +82,9 @@ type Provider struct { HTTPClient *http.Client config *oauth2.Config providerName string + authURL string + tokenURL string + profileURL string } // Name gets the name used to retrieve this provider later. @@ -100,11 +106,9 @@ func (p *Provider) Debug(debug bool) {} // BeginAuth asks Patreon for an authentication end-point. func (p *Provider) BeginAuth(state string) (goth.Session, error) { - url := p.config.AuthCodeURL(state) - session := &Session{ - AuthURL: url, - } - return session, nil + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil } // FetchUser will go to Patreon and access basic information about the user. From 9e48f5f78367cc93c09f06438275e13401d8df42 Mon Sep 17 00:00:00 2001 From: Jleagle Date: Wed, 2 Nov 2022 12:42:46 +0000 Subject: [PATCH 12/17] Revert to ioutil --- providers/patreon/patreon.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 4413bb994..1612d367a 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "time" @@ -142,7 +143,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) } - bits, err := io.ReadAll(response.Body) + bits, err := ioutil.ReadAll(response.Body) if err != nil { return user, err } From a374e826b7aa7ab30ce07c0e26d245f4a11b23d1 Mon Sep 17 00:00:00 2001 From: Jleagle Date: Wed, 2 Nov 2022 12:43:19 +0000 Subject: [PATCH 13/17] Rename const --- providers/patreon/patreon.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 1612d367a..c0938241f 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -20,7 +20,7 @@ const ( // AccessTokenURL specifies Patreon's OAuth2 token endpoint (see https://tools.ietf.org/html/rfc6749#section-3.2). // See Example_refreshToken for examples. - accessTokenURL = "https://www.patreon.com/api/oauth2/token" + tokenURL = "https://www.patreon.com/api/oauth2/token" profileURL = "https://www.patreon.com/api/oauth2/v2/identity" ) @@ -59,7 +59,7 @@ const ( // You should always call `patreon.New` to get a new provider. Never try to // create one manually. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { - return NewCustomisedURL(clientKey, secret, callbackURL, authorizationURL, accessTokenURL, profileURL, scopes...) + return NewCustomisedURL(clientKey, secret, callbackURL, authorizationURL, tokenURL, profileURL, scopes...) } // NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to From efc3596867f2e0c6df67d9463c3e559a4e6b8b18 Mon Sep 17 00:00:00 2001 From: Jleagle Date: Wed, 2 Nov 2022 12:43:33 +0000 Subject: [PATCH 14/17] Reorder funcs --- providers/patreon/patreon.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index c0938241f..4f0170628 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -158,6 +158,22 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, err } +// 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) { + 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 +} + func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { c := &oauth2.Config{ ClientID: provider.ClientKey, @@ -202,19 +218,3 @@ func userFromReader(r io.Reader, user *goth.User) error { user.AvatarURL = u.Data.Attributes.ImageURL return nil } - -// 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) { - 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 -} From b4c30ce59e052d9525c545728494d7958562f0d7 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Thu, 3 Nov 2022 21:44:13 +0000 Subject: [PATCH 15/17] Add Patreon to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fd75a2517..501044da8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ $ go get github.com/markbates/goth * OneDrive * OpenID Connect (auto discovery) * Oura +* Patreon * Paypal * SalesForce * Shopify From 31298b8221ea70860ab65b9481073564f70a4b55 Mon Sep 17 00:00:00 2001 From: James Eagle Date: Thu, 3 Nov 2022 21:44:42 +0000 Subject: [PATCH 16/17] Order --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 501044da8..a954bb424 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ $ go get github.com/markbates/goth * Intercom * Kakao * Lastfm -* Linkedin * LINE +* Linkedin * Mailru * Meetup * MicrosoftOnline @@ -71,8 +71,8 @@ $ go get github.com/markbates/goth * Typetalk * Uber * VK -* Wepay * WeCom +* Wepay * Xero * Yahoo * Yammer From 13c75a4a4cfa9fcc4800ad419e9ff18e33c03792 Mon Sep 17 00:00:00 2001 From: Jleagle Date: Fri, 4 Nov 2022 13:35:17 +0000 Subject: [PATCH 17/17] Rename --- providers/patreon/patreon.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go index 4f0170628..9d52a7cac 100644 --- a/providers/patreon/patreon.go +++ b/providers/patreon/patreon.go @@ -114,12 +114,12 @@ func (p *Provider) BeginAuth(state string) (goth.Session, error) { // FetchUser will go to Patreon and access basic information about the user. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { - sesh := session.(*Session) + sess := session.(*Session) user := goth.User{ - AccessToken: sesh.AccessToken, + AccessToken: sess.AccessToken, Provider: p.Name(), - RefreshToken: sesh.RefreshToken, - ExpiresAt: sesh.ExpiresAt, + RefreshToken: sess.RefreshToken, + ExpiresAt: sess.ExpiresAt, } if user.AccessToken == "" { @@ -132,7 +132,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { return user, err } - req.Header.Add("authorization", "Bearer "+sesh.AccessToken) + req.Header.Add("authorization", "Bearer "+sess.AccessToken) response, err := p.Client().Do(req) if err != nil { return user, err