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.

github.go 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. // Package github implements the OAuth2 protocol for authenticating users through Github.
  2. // This package can be used as a reference implementation of an OAuth2 provider for Goth.
  3. package github
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "io/ioutil"
  11. "net/http"
  12. "net/url"
  13. "strconv"
  14. "strings"
  15. "github.com/markbates/goth"
  16. "golang.org/x/oauth2"
  17. )
  18. // These vars define the Authentication, Token, and API URLS for GitHub. If
  19. // using GitHub enterprise you should change these values before calling New.
  20. //
  21. // Examples:
  22. // github.AuthURL = "https://github.acme.com/login/oauth/authorize
  23. // github.TokenURL = "https://github.acme.com/login/oauth/access_token
  24. // github.ProfileURL = "https://github.acme.com/api/v3/user
  25. // github.EmailURL = "https://github.acme.com/api/v3/user/emails
  26. var (
  27. AuthURL = "https://github.com/login/oauth/authorize"
  28. TokenURL = "https://github.com/login/oauth/access_token"
  29. ProfileURL = "https://api.github.com/user"
  30. EmailURL = "https://api.github.com/user/emails"
  31. )
  32. // New creates a new Github provider, and sets up important connection details.
  33. // You should always call `github.New` to get a new Provider. Never try to create
  34. // one manually.
  35. func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
  36. p := &Provider{
  37. ClientKey: clientKey,
  38. Secret: secret,
  39. CallbackURL: callbackURL,
  40. providerName: "github",
  41. }
  42. p.config = newConfig(p, scopes)
  43. return p
  44. }
  45. // Provider is the implementation of `goth.Provider` for accessing Github.
  46. type Provider struct {
  47. ClientKey string
  48. Secret string
  49. CallbackURL string
  50. HTTPClient *http.Client
  51. config *oauth2.Config
  52. providerName string
  53. }
  54. // Name is the name used to retrieve this provider later.
  55. func (p *Provider) Name() string {
  56. return p.providerName
  57. }
  58. // SetName is to update the name of the provider (needed in case of multiple providers of 1 type)
  59. func (p *Provider) SetName(name string) {
  60. p.providerName = name
  61. }
  62. func (p *Provider) Client() *http.Client {
  63. return goth.HTTPClientWithFallBack(p.HTTPClient)
  64. }
  65. // Debug is a no-op for the github package.
  66. func (p *Provider) Debug(debug bool) {}
  67. // BeginAuth asks Github for an authentication end-point.
  68. func (p *Provider) BeginAuth(state string) (goth.Session, error) {
  69. url := p.config.AuthCodeURL(state)
  70. session := &Session{
  71. AuthURL: url,
  72. }
  73. return session, nil
  74. }
  75. // FetchUser will go to Github and access basic information about the user.
  76. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
  77. sess := session.(*Session)
  78. user := goth.User{
  79. AccessToken: sess.AccessToken,
  80. Provider: p.Name(),
  81. }
  82. if user.AccessToken == "" {
  83. // data is not yet retrieved since accessToken is still empty
  84. return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
  85. }
  86. response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
  87. if err != nil {
  88. return user, err
  89. }
  90. defer response.Body.Close()
  91. if response.StatusCode != http.StatusOK {
  92. return user, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode)
  93. }
  94. bits, err := ioutil.ReadAll(response.Body)
  95. if err != nil {
  96. return user, err
  97. }
  98. err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
  99. if err != nil {
  100. return user, err
  101. }
  102. err = userFromReader(bytes.NewReader(bits), &user)
  103. if err != nil {
  104. return user, err
  105. }
  106. if user.Email == "" {
  107. for _, scope := range p.config.Scopes {
  108. if strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "user:email" {
  109. user.Email, err = getPrivateMail(p, sess)
  110. if err != nil {
  111. return user, err
  112. }
  113. break
  114. }
  115. }
  116. }
  117. return user, err
  118. }
  119. func userFromReader(reader io.Reader, user *goth.User) error {
  120. u := struct {
  121. ID int `json:"id"`
  122. Email string `json:"email"`
  123. Bio string `json:"bio"`
  124. Name string `json:"name"`
  125. Login string `json:"login"`
  126. Picture string `json:"avatar_url"`
  127. Location string `json:"location"`
  128. }{}
  129. err := json.NewDecoder(reader).Decode(&u)
  130. if err != nil {
  131. return err
  132. }
  133. user.Name = u.Name
  134. user.NickName = u.Login
  135. user.Email = u.Email
  136. user.Description = u.Bio
  137. user.AvatarURL = u.Picture
  138. user.UserID = strconv.Itoa(u.ID)
  139. user.Location = u.Location
  140. return err
  141. }
  142. func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
  143. response, err := p.Client().Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
  144. if err != nil {
  145. if response != nil {
  146. response.Body.Close()
  147. }
  148. return email, err
  149. }
  150. defer response.Body.Close()
  151. if response.StatusCode != http.StatusOK {
  152. return email, fmt.Errorf("GitHub API responded with a %d trying to fetch user email", response.StatusCode)
  153. }
  154. var mailList = []struct {
  155. Email string `json:"email"`
  156. Primary bool `json:"primary"`
  157. Verified bool `json:"verified"`
  158. }{}
  159. err = json.NewDecoder(response.Body).Decode(&mailList)
  160. if err != nil {
  161. return email, err
  162. }
  163. for _, v := range mailList {
  164. if v.Primary && v.Verified {
  165. return v.Email, nil
  166. }
  167. }
  168. // can't get primary email - shouldn't be possible
  169. return
  170. }
  171. func newConfig(provider *Provider, scopes []string) *oauth2.Config {
  172. c := &oauth2.Config{
  173. ClientID: provider.ClientKey,
  174. ClientSecret: provider.Secret,
  175. RedirectURL: provider.CallbackURL,
  176. Endpoint: oauth2.Endpoint{
  177. AuthURL: AuthURL,
  178. TokenURL: TokenURL,
  179. },
  180. Scopes: []string{},
  181. }
  182. for _, scope := range scopes {
  183. c.Scopes = append(c.Scopes, scope)
  184. }
  185. return c
  186. }
  187. //RefreshToken refresh token is not provided by github
  188. func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
  189. return nil, errors.New("Refresh token is not provided by github")
  190. }
  191. //RefreshTokenAvailable refresh token is not provided by github
  192. func (p *Provider) RefreshTokenAvailable() bool {
  193. return false
  194. }