A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
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.
 
 
 
 
 

181 lines
4.9 KiB

  1. /*
  2. * Copyright © 2019-2020 A Bunch Tell LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "context"
  13. "errors"
  14. "fmt"
  15. "github.com/writeas/nerds/store"
  16. "github.com/writeas/slug"
  17. "net/http"
  18. "net/url"
  19. "strings"
  20. )
  21. type slackOauthClient struct {
  22. ClientID string
  23. ClientSecret string
  24. TeamID string
  25. CallbackLocation string
  26. HttpClient HttpClient
  27. }
  28. type slackExchangeResponse struct {
  29. OK bool `json:"ok"`
  30. AccessToken string `json:"access_token"`
  31. Scope string `json:"scope"`
  32. TeamName string `json:"team_name"`
  33. TeamID string `json:"team_id"`
  34. Error string `json:"error"`
  35. }
  36. type slackIdentity struct {
  37. Name string `json:"name"`
  38. ID string `json:"id"`
  39. Email string `json:"email"`
  40. }
  41. type slackTeam struct {
  42. Name string `json:"name"`
  43. ID string `json:"id"`
  44. }
  45. type slackUserIdentityResponse struct {
  46. OK bool `json:"ok"`
  47. User slackIdentity `json:"user"`
  48. Team slackTeam `json:"team"`
  49. Error string `json:"error"`
  50. }
  51. const (
  52. slackAuthLocation = "https://slack.com/oauth/authorize"
  53. slackExchangeLocation = "https://slack.com/api/oauth.access"
  54. slackIdentityLocation = "https://slack.com/api/users.identity"
  55. )
  56. var _ oauthClient = slackOauthClient{}
  57. func (c slackOauthClient) GetProvider() string {
  58. return "slack"
  59. }
  60. func (c slackOauthClient) GetClientID() string {
  61. return c.ClientID
  62. }
  63. func (c slackOauthClient) GetCallbackLocation() string {
  64. return c.CallbackLocation
  65. }
  66. func (c slackOauthClient) buildLoginURL(state string) (string, error) {
  67. u, err := url.Parse(slackAuthLocation)
  68. if err != nil {
  69. return "", err
  70. }
  71. q := u.Query()
  72. q.Set("client_id", c.ClientID)
  73. q.Set("scope", "identity.basic identity.email identity.team")
  74. q.Set("redirect_uri", c.CallbackLocation)
  75. q.Set("state", state)
  76. // If this param is not set, the user can select which team they
  77. // authenticate through and then we'd have to match the configured team
  78. // against the profile get. That is extra work in the post-auth phase
  79. // that we don't want to do.
  80. q.Set("team", c.TeamID)
  81. // The Slack OAuth docs don't explicitly list this one, but it is part of
  82. // the spec, so we include it anyway.
  83. q.Set("response_type", "code")
  84. u.RawQuery = q.Encode()
  85. return u.String(), nil
  86. }
  87. func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
  88. form := url.Values{}
  89. // The oauth.access documentation doesn't explicitly mention this
  90. // parameter, but it is part of the spec, so we include it anyway.
  91. // https://api.slack.com/methods/oauth.access
  92. form.Add("grant_type", "authorization_code")
  93. form.Add("redirect_uri", c.CallbackLocation)
  94. form.Add("code", code)
  95. req, err := http.NewRequest("POST", slackExchangeLocation, strings.NewReader(form.Encode()))
  96. if err != nil {
  97. return nil, err
  98. }
  99. req.WithContext(ctx)
  100. req.Header.Set("User-Agent", "writefreely")
  101. req.Header.Set("Accept", "application/json")
  102. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  103. req.SetBasicAuth(c.ClientID, c.ClientSecret)
  104. resp, err := c.HttpClient.Do(req)
  105. if err != nil {
  106. return nil, err
  107. }
  108. if resp.StatusCode != http.StatusOK {
  109. return nil, errors.New("unable to exchange code for access token")
  110. }
  111. var tokenResponse slackExchangeResponse
  112. if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
  113. return nil, err
  114. }
  115. if !tokenResponse.OK {
  116. return nil, errors.New(tokenResponse.Error)
  117. }
  118. return tokenResponse.TokenResponse(), nil
  119. }
  120. func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
  121. req, err := http.NewRequest("GET", slackIdentityLocation, nil)
  122. if err != nil {
  123. return nil, err
  124. }
  125. req.WithContext(ctx)
  126. req.Header.Set("User-Agent", "writefreely")
  127. req.Header.Set("Accept", "application/json")
  128. req.Header.Set("Authorization", "Bearer "+accessToken)
  129. resp, err := c.HttpClient.Do(req)
  130. if err != nil {
  131. return nil, err
  132. }
  133. if resp.StatusCode != http.StatusOK {
  134. return nil, errors.New("unable to inspect access token")
  135. }
  136. var inspectResponse slackUserIdentityResponse
  137. if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
  138. return nil, err
  139. }
  140. if !inspectResponse.OK {
  141. return nil, errors.New(inspectResponse.Error)
  142. }
  143. return inspectResponse.InspectResponse(), nil
  144. }
  145. func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
  146. return &InspectResponse{
  147. UserID: resp.User.ID,
  148. Username: fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.GenerateRandomString("0123456789bcdfghjklmnpqrstvwxyz", 5)),
  149. DisplayName: resp.User.Name,
  150. Email: resp.User.Email,
  151. }
  152. }
  153. func (resp slackExchangeResponse) TokenResponse() *TokenResponse {
  154. return &TokenResponse{
  155. AccessToken: resp.AccessToken,
  156. }
  157. }