You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

195 lines
5.1 KiB

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