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.
 
 
 
 
 

179 lines
4.8 KiB

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