本站源代码
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.

234 lines
6.2KB

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