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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ $ go get github.com/markbates/goth
* Patreon
* Paypal
* Reddit
* Reverb
* SalesForce
* Shopify
* Slack
Expand Down
3 changes: 3 additions & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/markbates/goth/providers/openidConnect"
"github.com/markbates/goth/providers/patreon"
"github.com/markbates/goth/providers/paypal"
"github.com/markbates/goth/providers/reverb"
"github.com/markbates/goth/providers/salesforce"
"github.com/markbates/goth/providers/seatalk"
"github.com/markbates/goth/providers/shopify"
Expand Down Expand Up @@ -148,6 +149,7 @@ func main() {
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"),
reverb.New(os.Getenv("REVERB_KEY"), os.Getenv("REVERB_SECRET"), "http://localhost:3000/auth/reverb/callback"),
// DingTalk provider
dingtalk.New(os.Getenv("DINGTALK_KEY"), os.Getenv("DINGTALK_SECRET"), "https://f7ca-103-148-203-253.ngrok-free.app/auth/dingtalk/callback", os.Getenv("DINGTALK_CORP_ID"), "openid", "corpid"),
)
Expand Down Expand Up @@ -198,6 +200,7 @@ func main() {
"openid-connect": "OpenID Connect",
"patreon": "Patreon",
"paypal": "Paypal",
"reverb": "Reverb",
"salesforce": "Salesforce",
"seatalk": "SeaTalk",
"shopify": "Shopify",
Expand Down
274 changes: 274 additions & 0 deletions providers/reverb/reverb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// Package reverb implements the OAuth2 protocol for authenticating users through Reverb.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package reverb

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/markbates/goth"
"golang.org/x/oauth2"
)

const (
accountURL = "https://reverb.com/api/my/account"
authURL = "https://reverb.com/oauth/authorize"
tokenURL = "https://reverb.com/oauth/access_token"
providerName = "reverb"
versionHeader = "3.0"
)

var (
errInvalidSession = errors.New("reverb: invalid session provided")
errNilOAuthConfig = errors.New("reverb: oauth config is nil")
errNilProvider = errors.New("reverb: provider is nil")
errNilResponseBody = errors.New("reverb: empty response body")
)

// Provider is the implementation of `goth.Provider` for accessing Reverb.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
}

// New creates a new Reverb provider and sets up important connection details.
// You should always call `reverb.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: providerName,
}
p.config = newConfig(p, scopes)
return p
}

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

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) {
if p == nil {
return
}

p.providerName = name
}

// Client is the HTTP client used for all fetch operations.
func (p *Provider) Client() *http.Client {
if p == nil {
return goth.HTTPClientWithFallBack(nil)
}

return goth.HTTPClientWithFallBack(p.HTTPClient)
}

// Debug is a no-op for the Reverb package.
func (p *Provider) Debug(debug bool) {
}

// BeginAuth asks Reverb for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
if p == nil {
return nil, errNilProvider
}

if p.config == nil {
p.config = newConfig(p, nil)
}

return &Session{
AuthURL: p.config.AuthCodeURL(state),
}, nil
}

// FetchUser will go to Reverb and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
if p == nil {
return goth.User{}, errNilProvider
}

user := goth.User{
Provider: p.Name(),
}

sess, ok := session.(*Session)
if !ok || sess == nil {
return user, errInvalidSession
}

user.AccessToken = sess.AccessToken
user.RefreshToken = sess.RefreshToken
user.ExpiresAt = sess.ExpiresAt

if user.AccessToken == "" {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}

request, err := http.NewRequest(http.MethodGet, accountURL, nil)
if err != nil {
return user, err
}

request.Header.Set("Authorization", "Bearer "+sess.AccessToken)
request.Header.Set("Accept", "application/json")
request.Header.Set("Accept-Version", versionHeader)

client := p.Client()
if client == nil {
return user, fmt.Errorf("%s cannot fetch user information without an HTTP client", p.providerName)
}

response, err := client.Do(request)
if err != nil {
if response != nil {
response.Body.Close()
}
return user, err
}

if response.Body == nil {
return user, errNilResponseBody
}

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)
}

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

if err := json.Unmarshal(payload, &user.RawData); err != nil {
return user, err
}

var account accountResponse
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.UseNumber()
if err := decoder.Decode(&account); err != nil {
return user, err
}

user.Email = account.Email
user.FirstName = account.FirstName
user.LastName = account.LastName

if fullName := strings.TrimSpace(strings.Join([]string{account.FirstName, account.LastName}, " ")); fullName != "" {
user.Name = fullName
}

if account.ProfileSlug != "" {
user.NickName = account.ProfileSlug
}

if account.UUID != "" {
user.UserID = account.UUID
} else if account.UserID != nil {
user.UserID = account.UserID.String()
}

if account.Shop != nil {
if account.Shop.Name != "" {
user.Description = account.Shop.Name
}

if account.Shop.Slug != "" && user.NickName == "" {
user.NickName = account.Shop.Slug
}
}

if account.Links.Avatar.Href != "" {
user.AvatarURL = account.Links.Avatar.Href
}

if account.ShippingRegionCode != "" {
user.Location = account.ShippingRegionCode
}

return user, nil
}

// RefreshTokenAvailable refresh token is provided by auth provider or not.
func (p *Provider) RefreshTokenAvailable() bool {
return p != nil
}

// RefreshToken get new access token based on the refresh token.
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
if p == nil {
return nil, errNilProvider
}

if p.config == nil {
return nil, errNilOAuthConfig
}

token := &oauth2.Token{RefreshToken: refreshToken}
ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token)
return ts.Token()
}

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,
},
Scopes: []string{},
}

if len(scopes) > 0 {
c.Scopes = append(c.Scopes, scopes...)
}

return c
}

type accountResponse struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
ProfileSlug string `json:"profile_slug"`
UUID string `json:"uuid"`
UserID *json.Number `json:"user_id"`
ShippingRegionCode string `json:"shipping_region_code"`
Shop *shopPayload `json:"shop"`
Links accountLinkPayload `json:"_links"`
}

type shopPayload struct {
ID *json.Number `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}

type accountLinkPayload struct {
Avatar struct {
Href string `json:"href"`
} `json:"avatar"`
}
Loading
Loading