Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add livechat provider #399

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ $ go get github.com/markbates/goth
* Lastfm
* Linkedin
* LINE
* LiveChat
* Mailru
* Meetup
* MicrosoftOnline
Expand Down
4 changes: 4 additions & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"github.com/markbates/goth/providers/livechat"
"html/template"
"net/http"
"os"
Expand Down Expand Up @@ -84,6 +85,8 @@ func main() {
spotify.New(os.Getenv("SPOTIFY_KEY"), os.Getenv("SPOTIFY_SECRET"), "http://localhost:3000/auth/spotify/callback"),
linkedin.New(os.Getenv("LINKEDIN_KEY"), os.Getenv("LINKEDIN_SECRET"), "http://localhost:3000/auth/linkedin/callback"),
line.New(os.Getenv("LINE_KEY"), os.Getenv("LINE_SECRET"), "http://localhost:3000/auth/line/callback", "profile", "openid", "email"),
livechat.New(os.Getenv("LIVECHAT_KEY"), os.Getenv("LIVECHAT_SECRET"), "http://localhost:3000/auth/livechat/callback", livechat.WithConsent()),

zdunecki marked this conversation as resolved.
Show resolved Hide resolved
lastfm.New(os.Getenv("LASTFM_KEY"), os.Getenv("LASTFM_SECRET"), "http://localhost:3000/auth/lastfm/callback"),
twitch.New(os.Getenv("TWITCH_KEY"), os.Getenv("TWITCH_SECRET"), "http://localhost:3000/auth/twitch/callback"),
dropbox.New(os.Getenv("DROPBOX_KEY"), os.Getenv("DROPBOX_SECRET"), "http://localhost:3000/auth/dropbox/callback"),
Expand Down Expand Up @@ -180,6 +183,7 @@ func main() {
m["lastfm"] = "Last FM"
m["linkedin"] = "Linkedin"
m["line"] = "LINE"
m["livechat"] = "LiveChat"
m["onedrive"] = "Onedrive"
m["azuread"] = "Azure AD"
m["microsoftonline"] = "Microsoft Online"
Expand Down
223 changes: 223 additions & 0 deletions providers/livechat/livechat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Package livechat implements the OAuth protocol for authenticating users through Livechat.
package livechat

import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"net/http"
"strings"
)

const (
authURL = "https://accounts.livechat.com"
tokenURL = "https://accounts.livechat.com/v2/token"
userURL = "https://accounts.livechat.com/v2/accounts/me"
defaultProviderName = "livechat"
)

// Account represents LiveChat account
type Account struct {
ID string `json:"account_id"`
Email string `json:"email"`
Name string `json:"name"`
Link string `json:"link"`
EmailVerified bool `json:"email_verified"`
AvatarURL string `json:"avatar_url"`
OrganizationID string `json:"organization_id"`
}

type RawUserData struct {
Region string `json:"region"`
OrganizationID string `json:"organization_id"`
}

// Provider is the implementation of `goth.Provider` for accessing Livechat
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client

config *oauth2.Config
providerName string

consent bool
}

type Option func(p *Provider)

func WithConsent() Option {
return func(p *Provider) {
p.consent = true
}
}

// New creates the new Livechat provider
func New(clientKey, secret, callbackURL string, opts ...Option) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: defaultProviderName,
}
p.config = newConfig(p)

for _, o := range opts {
o(p)
}
return p
}

// 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 is a no-op for the livechat package
func (p *Provider) Debug(debug bool) {}

// BeginAuth asks Livechat for an authentication end-point
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
var opts []oauth2.AuthCodeOption

if p.consent {
opts = append(opts, oauth2.SetAuthURLParam("prompt", "consent"))
}

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

// FetchUser will fetch basic information about Livechat user
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
s := session.(*Session)
user := goth.User{
AccessToken: s.AccessToken,
RefreshToken: s.RefreshToken,
Provider: p.Name(),
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)
}

account, err := FetchAccount(p.Client(), s.AccessToken)
if err != nil {
return user, err
}
setGothUser(account, &user)

parts := strings.Split(s.AccessToken, ":")
if len(parts) != 2 {
return user, errors.New("invalid_region")
}

var userDataMap map[string]interface{}
{
userData := &RawUserData{
Region: parts[0],
OrganizationID: account.OrganizationID,
}

jUserData, _ := json.Marshal(userData)
json.Unmarshal(jUserData, &userDataMap)
}

user.RawData = userDataMap

return user, err
}

func FetchAccount(c *http.Client, accessToken string) (*Account, error) {
if c == nil {
c = http.DefaultClient
}
req, err := http.NewRequest("GET", userURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var account *Account

if err := json.NewDecoder(resp.Body).Decode(&account); err != nil {
return nil, err
}

return account, nil
}

func setGothUser(a *Account, user *goth.User) {
user.UserID = a.ID
user.Name = a.Name
user.FirstName, user.LastName = splitName(a.Name)
user.Email = a.Email
user.AvatarURL = a.AvatarURL
}

func splitName(name string) (string, string) {
nameSplit := strings.SplitN(name, " ", 2)
firstName := nameSplit[0]

var lastName string
if len(nameSplit) == 2 {
lastName = nameSplit[1]
}

return firstName, lastName
}

func newConfig(provider *Provider) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
// The list of resources and actions is automatically created based on the scopes selected for your app in Developer Console
Scopes: []string{},
}

return c
}

// 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(context.Background(), token)
newToken, err := ts.Token()
if err != nil {
return nil, err
}
return newToken, err
}

// RefreshTokenAvailable refresh token is provided by auth provider or not
func (p *Provider) RefreshTokenAvailable() bool {
return true
}
51 changes: 51 additions & 0 deletions providers/livechat/livechat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package livechat

import (
"github.com/markbates/goth"
"github.com/stretchr/testify/assert"
"os"
"testing"
)

func Test_New(t *testing.T) {
t.Parallel()
a := assert.New(t)
p := provider()

a.Equal(p.ClientKey, os.Getenv("LIVECHAT_KEY"))
a.Equal(p.Secret, os.Getenv("LIVECHAT_SECRET"))
a.Equal(p.CallbackURL, "/foo")
}

func Test_Implements_Provider(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, "accounts.livechat.com")
}

func Test_SessionFromJSON(t *testing.T) {
t.Parallel()
a := assert.New(t)

p := provider()
session, err := p.UnmarshalSession(`{"AuthURL":"https://accounts.livechat.com","AccessToken":"1234567890"}`)
a.NoError(err)

s := session.(*Session)
a.Equal(s.AuthURL, "https://accounts.livechat.com")
a.Equal(s.AccessToken, "1234567890")
}

func provider() *Provider {
return New(os.Getenv("LIVECHAT_KEY"), os.Getenv("LIVECHAT_SECRET"), "/foo")
}
61 changes: 61 additions & 0 deletions providers/livechat/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package livechat

import (
"encoding/json"
"errors"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"strings"
"time"
)

// Session stores data during the auth process with intercom.
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 intercom provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}

// Authorize the session with intercom 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)
token, err := p.config.Exchange(oauth2.NoContext, 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 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
}