Open ianlopshire opened 4 years ago
Locally I need to connect to the new Instagram graph flow too. As documented here https://developers.facebook.com/docs/instagram-basic-display-api/getting-started
So I have duplicated the existing provider and made changes as follows. Note that the session.go
already present will work out of the box with these changes.
user_profile
and user_media
.I also see the issue of the naming of the provider but adding this here in case it can help. I probably should fork & create a PR. Likely will if I can enough requests to do so. Thanks K
// Package instagram implements the OAuth2 protocol for authenticating users through Instagram.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package instagram
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
var (
authURL = "https://api.instagram.com/oauth/authorize/"
tokenURL = "https://api.instagram.com/oauth/access_token"
endPointProfile = "https://graph.instagram.com/me?fields=account_type,id,username"
refreshTokenURL = "https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token="
)
// New creates a new Instagram provider, and sets up important connection details.
// You should always call `instagram.New` to get a new Provider. Never try to craete
// one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "instagram",
}
p.config = newConfig(p, scopes)
return p
}
// Provider is the implementation of `goth.Provider` for accessing Instagram
type Provider struct {
ClientKey string
Secret string
CallbackURL string
UserAgent string
HTTPClient *http.Client
config *oauth2.Config
providerName string
}
// 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 TODO
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Instagram 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 Instagram and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
}
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)
}
// Request a long lived access token now that we have a successful short lived token
exchangeURI := fmt.Sprintf(
"https://graph.instagram.com/access_token?grant_type=ig_exchange_token&client_secret=%s&access_token=%s",
p.config.ClientSecret,
url.QueryEscape(sess.AccessToken),
)
exchangeResponse, err := p.Client().Get(exchangeURI)
if err != nil {
return user, err
}
defer exchangeResponse.Body.Close()
exchange := struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}{}
err = json.NewDecoder(exchangeResponse.Body).Decode(&exchange)
if err != nil {
return user, err
}
user.AccessToken = exchange.AccessToken
user.ExpiresAt = time.Now().Add(time.Second * time.Duration(exchange.ExpiresIn))
// Check if user_media scope is present and update the user fields to return
uri := endPointProfile
for _, scope := range p.config.Scopes {
if scope == "user_media" {
uri = uri + ",media_count"
break
}
}
response, err := p.Client().Get(uri + "&access_token=" + url.QueryEscape(user.AccessToken))
if err != nil {
return user, err
}
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)
}
err = userFromReader(response.Body, &user)
return user, err
}
func userFromReader(reader io.Reader, user *goth.User) error {
u := struct {
AccountType string `json:"account_type"`
ID string `json:"id"`
MediaCount int `json:"media_count"`
UserName string `json:"username"`
}{}
err := json.NewDecoder(reader).Decode(&u)
if err != nil {
return err
}
user.UserID = u.ID
user.NickName = u.UserName
return 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: authURL,
TokenURL: tokenURL,
},
Scopes: []string{
"user_profile",
"user_media",
},
}
defaultScopes := map[string]struct{}{
"user_profile": {},
"user_media": {},
}
for _, scope := range scopes {
if _, exists := defaultScopes[scope]; !exists {
c.Scopes = append(c.Scopes, scope)
}
}
return c
}
// RefreshToken will refresh an instagram long lived token.
// Refer https://developers.facebook.com/docs/instagram-basic-display-api/guides/long-lived-access-tokens
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
refreshResponse, err := p.Client().Get(refreshTokenURL + refreshToken)
if err != nil {
return nil, err
}
defer refreshResponse.Body.Close()
refresh := struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}{}
err = json.NewDecoder(refreshResponse.Body).Decode(&refresh)
if err != nil {
return nil, err
}
Expires := time.Now().Add(time.Second * time.Duration(refresh.ExpiresIn))
return &oauth2.Token{
AccessToken: refresh.AccessToken,
Expiry: Expires,
}, nil
}
// RefreshTokenAvailable refresh token
func (p *Provider) RefreshTokenAvailable() bool {
return true
}
@kylehqcom Don't know if you got any requests for the PR, but I'm blocked by the same issue, so I think a PR would be great!
Hey @yelskiy, you're not completely blocked, you can use the code above and place into your project. eg
import (
"github.com/markbates/goth"
"gitlab.com/kylehqcom/***/***/lib/auth/provider/instagram"
)
instagramProvider := instagram.New(instagramClientID, instagramClientSecret, instagramCallbackURL)
goth.UseProviders(instagramProvider)
I am using this to connect all ok locally, not in production. Still not prime time as I needed for a side project.
Thanks @kylehqcom! I should have mentioned in my comment that I am using the code you provided for my local development to keep moving forward.
The existing Instagram API is deprecated and will be Sunset on June 29th, 2020.
The replacement is the Instagram Basic Display API. This is an entirely new OAuth provider.
One potential issue I see is the naming of the provider. We will want to keep the existing Instagram provider in place.