1// Package facebook implements the OAuth2 protocol for authenticating users through Facebook. 2// This package can be used as a reference implementation of an OAuth2 provider for Goth. 3package facebook 4 5import ( 6 "bytes" 7 "crypto/hmac" 8 "crypto/sha256" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "io/ioutil" 15 "net/http" 16 "net/url" 17 "strings" 18 19 "github.com/markbates/goth" 20 "golang.org/x/oauth2" 21) 22 23const ( 24 authURL string = "https://www.facebook.com/dialog/oauth" 25 tokenURL string = "https://graph.facebook.com/oauth/access_token" 26 endpointProfile string = "https://graph.facebook.com/me?fields=" 27) 28 29// New creates a new Facebook provider, and sets up important connection details. 30// You should always call `facebook.New` to get a new Provider. Never try to create 31// one manually. 32func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { 33 p := &Provider{ 34 ClientKey: clientKey, 35 Secret: secret, 36 CallbackURL: callbackURL, 37 providerName: "facebook", 38 } 39 p.config = newConfig(p, scopes) 40 p.Fields = "email,first_name,last_name,link,about,id,name,picture,location" 41 return p 42} 43 44// Provider is the implementation of `goth.Provider` for accessing Facebook. 45type Provider struct { 46 ClientKey string 47 Secret string 48 CallbackURL string 49 HTTPClient *http.Client 50 Fields string 51 config *oauth2.Config 52 providerName string 53} 54 55// Name is the name used to retrieve this provider later. 56func (p *Provider) Name() string { 57 return p.providerName 58} 59 60// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) 61func (p *Provider) SetName(name string) { 62 p.providerName = name 63} 64 65// SetCustomFields sets the fields used to return information 66// for a user. 67// 68// A list of available field values can be found at 69// https://developers.facebook.com/docs/graph-api/reference/user 70func (p *Provider) SetCustomFields(fields []string) *Provider { 71 p.Fields = strings.Join(fields, ",") 72 return p 73} 74 75func (p *Provider) Client() *http.Client { 76 return goth.HTTPClientWithFallBack(p.HTTPClient) 77} 78 79// Debug is a no-op for the facebook package. 80func (p *Provider) Debug(debug bool) {} 81 82// BeginAuth asks Facebook for an authentication end-point. 83func (p *Provider) BeginAuth(state string) (goth.Session, error) { 84 authUrl := p.config.AuthCodeURL(state) 85 session := &Session{ 86 AuthURL: authUrl, 87 } 88 return session, nil 89} 90 91// FetchUser will go to Facebook and access basic information about the user. 92func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { 93 sess := session.(*Session) 94 user := goth.User{ 95 AccessToken: sess.AccessToken, 96 Provider: p.Name(), 97 ExpiresAt: sess.ExpiresAt, 98 } 99 100 if user.AccessToken == "" { 101 // data is not yet retrieved since accessToken is still empty 102 return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) 103 } 104 105 // always add appsecretProof to make calls more protected 106 // https://github.com/markbates/goth/issues/96 107 // https://developers.facebook.com/docs/graph-api/securing-requests 108 hash := hmac.New(sha256.New, []byte(p.Secret)) 109 hash.Write([]byte(sess.AccessToken)) 110 appsecretProof := hex.EncodeToString(hash.Sum(nil)) 111 112 reqUrl := fmt.Sprint( 113 endpointProfile, 114 p.Fields, 115 "&access_token=", 116 url.QueryEscape(sess.AccessToken), 117 "&appsecret_proof=", 118 appsecretProof, 119 ) 120 response, err := p.Client().Get(reqUrl) 121 if err != nil { 122 return user, err 123 } 124 defer response.Body.Close() 125 126 if response.StatusCode != http.StatusOK { 127 return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) 128 } 129 130 bits, err := ioutil.ReadAll(response.Body) 131 if err != nil { 132 return user, err 133 } 134 135 err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) 136 if err != nil { 137 return user, err 138 } 139 140 err = userFromReader(bytes.NewReader(bits), &user) 141 return user, err 142} 143 144func userFromReader(reader io.Reader, user *goth.User) error { 145 u := struct { 146 ID string `json:"id"` 147 Email string `json:"email"` 148 About string `json:"about"` 149 Name string `json:"name"` 150 FirstName string `json:"first_name"` 151 LastName string `json:"last_name"` 152 Link string `json:"link"` 153 Picture struct { 154 Data struct { 155 URL string `json:"url"` 156 } `json:"data"` 157 } `json:"picture"` 158 Location struct { 159 Name string `json:"name"` 160 } `json:"location"` 161 }{} 162 163 err := json.NewDecoder(reader).Decode(&u) 164 if err != nil { 165 return err 166 } 167 168 user.Name = u.Name 169 user.FirstName = u.FirstName 170 user.LastName = u.LastName 171 user.NickName = u.Name 172 user.Email = u.Email 173 user.Description = u.About 174 user.AvatarURL = u.Picture.Data.URL 175 user.UserID = u.ID 176 user.Location = u.Location.Name 177 178 return err 179} 180 181func newConfig(provider *Provider, scopes []string) *oauth2.Config { 182 c := &oauth2.Config{ 183 ClientID: provider.ClientKey, 184 ClientSecret: provider.Secret, 185 RedirectURL: provider.CallbackURL, 186 Endpoint: oauth2.Endpoint{ 187 AuthURL: authURL, 188 TokenURL: tokenURL, 189 }, 190 Scopes: []string{ 191 "email", 192 }, 193 } 194 195 defaultScopes := map[string]struct{}{ 196 "email": {}, 197 } 198 199 for _, scope := range scopes { 200 if _, exists := defaultScopes[scope]; !exists { 201 c.Scopes = append(c.Scopes, scope) 202 } 203 } 204 205 return c 206} 207 208//RefreshToken refresh token is not provided by facebook 209func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { 210 return nil, errors.New("Refresh token is not provided by facebook") 211} 212 213//RefreshTokenAvailable refresh token is not provided by facebook 214func (p *Provider) RefreshTokenAvailable() bool { 215 return false 216} 217