Skip to content

Commit

Permalink
Merge pull request #29 from amencarini/add-spotify
Browse files Browse the repository at this point in the history
Adds Spotify as an OAuth2 provider
  • Loading branch information
matryer committed Jul 13, 2015
2 parents 64e995f + d56e0a1 commit 82c20a1
Show file tree
Hide file tree
Showing 4 changed files with 425 additions and 0 deletions.
118 changes: 118 additions & 0 deletions providers/spotify/spotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package spotify

import (
"net/http"

"github.com/stretchr/gomniauth"
"github.com/stretchr/gomniauth/common"
"github.com/stretchr/gomniauth/oauth2"
"github.com/stretchr/objx"
)

const (
spotifyDefaultScope string = "user-read-email"
spotifyName string = "spotify"
spotifyDisplayName string = "Spotify"
spotifyAuthURL string = "https://accounts.spotify.com/authorize"
spotifyTokenURL string = "https://accounts.spotify.com/api/token"
spotifyEndpointProfile string = "https://api.spotify.com/v1/me"
)

// SpotifyProvider implements the Provider interface and provides Spotify
// OAuth2 communication capabilities.
type SpotifyProvider struct {
config *common.Config
tripperFactory common.TripperFactory
}

func New(clientId, clientSecret, redirectUrl string) *SpotifyProvider {

p := new(SpotifyProvider)
p.config = &common.Config{Map: objx.MSI(
oauth2.OAuth2KeyAuthURL, spotifyAuthURL,
oauth2.OAuth2KeyTokenURL, spotifyTokenURL,
oauth2.OAuth2KeyClientID, clientId,
oauth2.OAuth2KeySecret, clientSecret,
oauth2.OAuth2KeyRedirectUrl, redirectUrl,
oauth2.OAuth2KeyScope, spotifyDefaultScope,
oauth2.OAuth2KeyAccessType, oauth2.OAuth2AccessTypeOnline,
oauth2.OAuth2KeyApprovalPrompt, oauth2.OAuth2ApprovalPromptAuto,
oauth2.OAuth2KeyResponseType, oauth2.OAuth2KeyCode)}
return p
}

// TripperFactory gets an OAuth2TripperFactory
func (provider *SpotifyProvider) TripperFactory() common.TripperFactory {

if provider.tripperFactory == nil {
provider.tripperFactory = new(oauth2.OAuth2TripperFactory)
}

return provider.tripperFactory
}

// PublicData gets a public readable view of this provider.
func (provider *SpotifyProvider) PublicData(options map[string]interface{}) (interface{}, error) {
return gomniauth.ProviderPublicData(provider, options)
}

// Name is the unique name for this provider.
func (provider *SpotifyProvider) Name() string {
return spotifyName
}

// DisplayName is the human readable name for this provider.
func (provider *SpotifyProvider) DisplayName() string {
return spotifyDisplayName
}

// GetBeginAuthURL gets the URL that the client must visit in order
// to begin the authentication process.
//
// The state argument contains anything you wish to have sent back to your
// callback endpoint.
// The options argument takes any options used to configure the auth request
// sent to the provider. In the case of OAuth2, the options map can contain:
// 1. A "scope" key providing the desired scope(s). It will be merged with the default scope.
func (provider *SpotifyProvider) GetBeginAuthURL(state *common.State, options objx.Map) (string, error) {
if options != nil {
scope := oauth2.MergeScopes(options.Get(oauth2.OAuth2KeyScope).Str(), spotifyDefaultScope)
provider.config.Set(oauth2.OAuth2KeyScope, scope)
}
return oauth2.GetBeginAuthURLWithBase(provider.config.Get(oauth2.OAuth2KeyAuthURL).Str(), state, provider.config)
}

// Get makes an authenticated request and returns the data in the
// response as a data map.
func (provider *SpotifyProvider) Get(creds *common.Credentials, endpoint string) (objx.Map, error) {
return oauth2.Get(provider, creds, endpoint)
}

// GetUser uses the specified common.Credentials to access the users profile
// from the remote provider, and builds the appropriate User object.
func (provider *SpotifyProvider) GetUser(creds *common.Credentials) (common.User, error) {

profileData, err := provider.Get(creds, spotifyEndpointProfile)

if err != nil {
return nil, err
}

// build user
user := NewUser(profileData, creds, provider)

return user, nil
}

// CompleteAuth takes a map of arguments that are used to
// complete the authorisation process, completes it, and returns
// the appropriate Credentials.
func (provider *SpotifyProvider) CompleteAuth(data objx.Map) (*common.Credentials, error) {
return oauth2.CompleteAuth(provider.TripperFactory(), data, provider.config, provider)
}

// GetClient returns an authenticated http.Client that can be used to make requests to
// protected spotify resources
func (provider *SpotifyProvider) GetClient(creds *common.Credentials) (*http.Client, error) {
return oauth2.GetClient(provider.TripperFactory(), creds, provider)
}
153 changes: 153 additions & 0 deletions providers/spotify/spotify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package spotify

import (
"io/ioutil"
"net/http"
"strings"
"testing"

"github.com/stretchr/gomniauth/common"
"github.com/stretchr/gomniauth/oauth2"
"github.com/stretchr/gomniauth/test"
"github.com/stretchr/objx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestGitHubImplementrsProvider(t *testing.T) {

var provider common.Provider
provider = new(SpotifyProvider)

assert.NotNil(t, provider)

}

func TestGetUser(t *testing.T) {

g := New("clientID", "secret", "http://myapp.com/")
creds := &common.Credentials{Map: objx.MSI()}

testTripperFactory := new(test.TestTripperFactory)
testTripper := new(test.TestTripper)
testTripperFactory.On("NewTripper", mock.Anything, g).Return(testTripper, nil)
testResponse := new(http.Response)
testResponse.Header = make(http.Header)
testResponse.Header.Set("Content-Type", "application/json")
testResponse.StatusCode = 200
testResponse.Body = ioutil.NopCloser(strings.NewReader(`{
"display_name":"JMWizzler",
"email":"email@example.com",
"external_urls":{
"spotify":"https://open.spotify.com/user/wizzler"
},
"href":"https://api.spotify.com/v1/users/wizzler",
"id":"wizzler",
"images":[{
"height":null,
"url":"https://fbcdn.example.com/2330_n.jpg",
"width":null
}],
"type":"user",
"uri":"spotify:user:wizzler"
}`))
testTripper.On("RoundTrip", mock.Anything).Return(testResponse, nil)

g.tripperFactory = testTripperFactory

user, err := g.GetUser(creds)

if assert.NoError(t, err) && assert.NotNil(t, user) {

assert.Equal(t, user.Name(), "JMWizzler")
assert.Equal(t, user.AuthCode(), "") // doesn't come from spotify
assert.Equal(t, user.Nickname(), "") // doesn't come from spotify
assert.Equal(t, user.Email(), "email@example.com")
assert.Equal(t, user.AvatarURL(), "https://fbcdn.example.com/2330_n.jpg")
assert.Equal(t, user.Data()["href"], "https://api.spotify.com/v1/users/wizzler")

spotifyCreds := user.ProviderCredentials()[spotifyName]
if assert.NotNil(t, spotifyCreds) {
assert.Equal(t, "wizzler", spotifyCreds.Get(common.CredentialsKeyID).Str())
}

}

}

func TestNewspotify(t *testing.T) {

g := New("clientID", "secret", "http://myapp.com/")

if assert.NotNil(t, g) {

// check config
if assert.NotNil(t, g.config) {

assert.Equal(t, "clientID", g.config.Get(oauth2.OAuth2KeyClientID).Data())
assert.Equal(t, "secret", g.config.Get(oauth2.OAuth2KeySecret).Data())
assert.Equal(t, "http://myapp.com/", g.config.Get(oauth2.OAuth2KeyRedirectUrl).Data())
assert.Equal(t, spotifyDefaultScope, g.config.Get(oauth2.OAuth2KeyScope).Data())

assert.Equal(t, spotifyAuthURL, g.config.Get(oauth2.OAuth2KeyAuthURL).Data())
assert.Equal(t, spotifyTokenURL, g.config.Get(oauth2.OAuth2KeyTokenURL).Data())

}

}

}

func TestspotifyTripperFactory(t *testing.T) {

g := New("clientID", "secret", "http://myapp.com/")
g.tripperFactory = nil

f := g.TripperFactory()

if assert.NotNil(t, f) {
assert.Equal(t, f, g.tripperFactory)
}

}

func TestspotifyName(t *testing.T) {
g := New("clientID", "secret", "http://myapp.com/")
assert.Equal(t, spotifyName, g.Name())
}

func TestspotifyGetBeginAuthURL(t *testing.T) {

common.SetSecurityKey("ABC123")

state := &common.State{Map: objx.MSI("after", "http://www.stretchr.com/")}

g := New("clientID", "secret", "http://myapp.com/")

url, err := g.GetBeginAuthURL(state, nil)

if assert.NoError(t, err) {
assert.Contains(t, url, "client_id=clientID")
assert.Contains(t, url, "redirect_uri=http%3A%2F%2Fmyapp.com%2F")
assert.Contains(t, url, "scope="+spotifyDefaultScope)
assert.Contains(t, url, "access_type="+oauth2.OAuth2AccessTypeOnline)
assert.Contains(t, url, "approval_prompt="+oauth2.OAuth2ApprovalPromptAuto)
}

state = &common.State{Map: objx.MSI("after", "http://www.stretchr.com/")}

g = New("clientID", "secret", "http://myapp.com/")

url, err = g.GetBeginAuthURL(state, objx.MSI(oauth2.OAuth2KeyScope, "avatar"))

if assert.NoError(t, err) {
assert.Contains(t, url, "client_id=clientID")
assert.Contains(t, url, "redirect_uri=http%3A%2F%2Fmyapp.com%2F")
assert.Contains(t, url, "scope=avatar+"+spotifyDefaultScope)
assert.Contains(t, url, "access_type="+oauth2.OAuth2AccessTypeOnline)
assert.Contains(t, url, "approval_prompt="+oauth2.OAuth2ApprovalPromptAuto)
}

}

// test that scopes get added with spaces
87 changes: 87 additions & 0 deletions providers/spotify/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package spotify

import (
"strconv"

"github.com/stretchr/gomniauth/common"
"github.com/stretchr/objx"
)

const (
spotifyKeyID string = "id"
spotifyKeyName string = "display_name"
spotifyKeyNickname string = "username"
spotifyKeyEmail string = "email"
spotifyKeyPictureUrl string = "images[0].url"
)

type User struct {
data objx.Map
}

// NewUser builds a new User object for Github.
func NewUser(data objx.Map, creds *common.Credentials, provider common.Provider) *User {
user := &User{data}

creds.Set(common.CredentialsKeyID, data[spotifyKeyID])
// set provider credentials
user.data[common.UserKeyProviderCredentials] = map[string]*common.Credentials{
provider.Name(): creds,
}

return user
}

// Email gets the users email address.
func (u *User) Email() string {
return u.Data().Get(spotifyKeyEmail).Str()
}

// Name gets the users full name.
func (u *User) Name() string {
return u.Data().Get(spotifyKeyName).Str()

}

// Nickname gets the users nickname or username.
func (u *User) Nickname() string {
return u.Data().Get(spotifyKeyNickname).Str()

}

// AvatarURL gets the URL of an image representing the user.
func (u *User) AvatarURL() string {
return u.Data().Get(spotifyKeyPictureUrl).Str()
}

// ProviderCredentials gets a map of Credentials (by provider name).
func (u *User) ProviderCredentials() map[string]*common.Credentials {
return u.Data().Get(common.UserKeyProviderCredentials).Data().(map[string]*common.Credentials)
}

// IDForProvider gets the ID value for the specified provider name for
// this user from the ProviderCredentials data.
func (u *User) IDForProvider(provider string) string {
id := u.ProviderCredentials()[provider].Get(common.CredentialsKeyID).Data()
switch id.(type) {
case string:
return id.(string)
case float64:
return strconv.FormatFloat(id.(float64), 'f', 0, 64)
}
return ""
}

// AuthCode gets this user's globally unique ID (generated by the host program)
func (u *User) AuthCode() string {
return u.Data().Get(common.UserKeyAuthCode).Str()
}

// GetValue gets any User field by name.
func (u *User) Data() objx.Map {
return u.data
}

func (u *User) PublicData(options map[string]interface{}) (publicData interface{}, err error) {
return u.data, nil
}
Loading

0 comments on commit 82c20a1

Please sign in to comment.