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 6.3KB

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