forked from stretchr/gomniauth
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding instagram provider as requested in stretchr#10
- Loading branch information
Showing
4 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package instagram | ||
|
||
import ( | ||
"github.com/stretchr/gomniauth" | ||
"github.com/stretchr/gomniauth/common" | ||
"github.com/stretchr/gomniauth/oauth2" | ||
"github.com/stretchr/objx" | ||
"net/http" | ||
) | ||
|
||
const ( | ||
instagramDefaultScope string = "basic" | ||
instagramName string = "instagram" | ||
instagramDisplayName string = "Instagram" | ||
instagramAuthURL string = "https://api.instagram.com/oauth/authorize" | ||
instagramTokenURL string = "https://api.instagram.com/oauth/access_token" | ||
instagramEndpointProfile string = "https://api.instagram.com/v1/users/self" | ||
) | ||
|
||
// InstagramProvider implements the Provider interface and provides Instagram | ||
// OAuth2 communication capabilities. | ||
type InstagramProvider struct { | ||
config *common.Config | ||
tripperFactory common.TripperFactory | ||
} | ||
|
||
func New(clientId, clientSecret, redirectUrl string) *InstagramProvider { | ||
|
||
p := new(InstagramProvider) | ||
p.config = &common.Config{Map: objx.MSI( | ||
oauth2.OAuth2KeyAuthURL, instagramAuthURL, | ||
oauth2.OAuth2KeyTokenURL, instagramTokenURL, | ||
oauth2.OAuth2KeyClientID, clientId, | ||
oauth2.OAuth2KeySecret, clientSecret, | ||
oauth2.OAuth2KeyRedirectUrl, redirectUrl, | ||
oauth2.OAuth2KeyScope, instagramDefaultScope, | ||
oauth2.OAuth2KeyAccessType, oauth2.OAuth2AccessTypeOnline, | ||
oauth2.OAuth2KeyApprovalPrompt, oauth2.OAuth2ApprovalPromptAuto, | ||
oauth2.OAuth2KeyResponseType, oauth2.OAuth2KeyCode)} | ||
return p | ||
} | ||
|
||
// TripperFactory gets an OAuth2TripperFactory | ||
func (provider *InstagramProvider) 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 *InstagramProvider) PublicData(options map[string]interface{}) (interface{}, error) { | ||
return gomniauth.ProviderPublicData(provider, options) | ||
} | ||
|
||
// Name is the unique name for this provider. | ||
func (provider *InstagramProvider) Name() string { | ||
return instagramName | ||
} | ||
|
||
// DisplayName is the human readable name for this provider. | ||
func (provider *InstagramProvider) DisplayName() string { | ||
return instagramDisplayName | ||
} | ||
|
||
// 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 *InstagramProvider) GetBeginAuthURL(state *common.State, options objx.Map) (string, error) { | ||
if options != nil { | ||
scope := oauth2.MergeScopes(options.Get(oauth2.OAuth2KeyScope).Str(), instagramDefaultScope) | ||
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 *InstagramProvider) 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 *InstagramProvider) GetUser(creds *common.Credentials) (common.User, error) { | ||
|
||
profileData, err := provider.Get(creds, instagramEndpointProfile) | ||
|
||
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 *InstagramProvider) 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 Instagram resources | ||
func (provider *InstagramProvider) GetClient(creds *common.Credentials) (*http.Client, error) { | ||
return oauth2.GetClient(provider.TripperFactory(), creds, provider) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package instagram | ||
|
||
import ( | ||
"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" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestInstagramImplementrsProvider(t *testing.T) { | ||
|
||
var provider common.Provider | ||
provider = new(InstagramProvider) | ||
|
||
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(`{ | ||
"meta": { | ||
"code": 200 | ||
}, | ||
"data": { | ||
"username": "maggit", | ||
"bio": "Programer who loves golang", | ||
"website": "http://website.com", | ||
"profile_picture": "http://myinsta.com", | ||
"full_name": "Raquel H", | ||
"counts": { | ||
"media": 1232323, | ||
"followed_by": 123, | ||
"follows": 123 | ||
}, | ||
"id": "12345678" | ||
} | ||
}`)) | ||
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(), "Raquel H") | ||
assert.Equal(t, user.AuthCode(), "") // doesn't come from instagram | ||
assert.Equal(t, user.Nickname(), "maggit") | ||
assert.Equal(t, user.AvatarURL(), "http://myinsta.com") | ||
assert.Equal(t, user.Data()["website"], "http://website.com") | ||
|
||
instagramCreds := user.ProviderCredentials()[instagramName] | ||
if assert.NotNil(t, instagramCreds) { | ||
assert.Equal(t, "uniqueid", instagramCreds.Get(common.CredentialsKeyID).Str()) | ||
} | ||
|
||
} | ||
|
||
} | ||
|
||
func TestNewInstagram(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, instagramDefaultScope, g.config.Get(oauth2.OAuth2KeyScope).Data()) | ||
|
||
assert.Equal(t, instagramAuthURL, g.config.Get(oauth2.OAuth2KeyAuthURL).Data()) | ||
assert.Equal(t, instagramTokenURL, g.config.Get(oauth2.OAuth2KeyTokenURL).Data()) | ||
|
||
} | ||
|
||
} | ||
|
||
} | ||
|
||
func TestInstagramTripperFactory(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 TestInstagramName(t *testing.T) { | ||
g := New("clientID", "secret", "http://myapp.com/") | ||
assert.Equal(t, instagramName, g.Name()) | ||
} | ||
|
||
func TestInstagramGetBeginAuthURL(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="+instagramDefaultScope) | ||
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+"+instagramDefaultScope) | ||
assert.Contains(t, url, "access_type="+oauth2.OAuth2AccessTypeOnline) | ||
assert.Contains(t, url, "approval_prompt="+oauth2.OAuth2ApprovalPromptAuto) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package instagram | ||
|
||
import ( | ||
"github.com/stretchr/gomniauth/common" | ||
"github.com/stretchr/objx" | ||
"strconv" | ||
) | ||
|
||
const ( | ||
instagramKeyID string = "id" | ||
instagramKeyName string = "full_name" | ||
instagramKeyNickname string = "username" | ||
instagramKeyAvatarUrl string = "profile_picture" | ||
) | ||
|
||
type User struct { | ||
data objx.Map | ||
} | ||
|
||
// NewUser builds a new User object for Instagram. | ||
func NewUser(data objx.Map, creds *common.Credentials, provider common.Provider) *User { | ||
user := &User{data} | ||
|
||
creds.Set(common.CredentialsKeyID, data[instagramKeyID]) | ||
// set provider credentials | ||
user.data[common.UserKeyProviderCredentials] = map[string]*common.Credentials{ | ||
provider.Name(): creds, | ||
} | ||
|
||
return user | ||
} | ||
|
||
// Name gets the users full name. | ||
func (u *User) Name() string { | ||
return u.Data().Get(instagramKeyName).Str() | ||
|
||
} | ||
|
||
// Nickname gets the users nickname or username. | ||
func (u *User) Nickname() string { | ||
return u.Data().Get(instagramKeyNickname).Str() | ||
|
||
} | ||
|
||
// Instagram API doesn't return email | ||
func (u *User) Email() string { | ||
return "" | ||
} | ||
|
||
// AvatarURL gets the URL of an image representing the user. | ||
func (u *User) AvatarURL() string { | ||
return u.Data().Get(instagramKeyAvatarUrl).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 | ||
} |
Oops, something went wrong.