client.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package indieauth
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/owncast/owncast/core/data"
  13. "github.com/owncast/owncast/utils"
  14. "github.com/pkg/errors"
  15. log "github.com/sirupsen/logrus"
  16. )
  17. var (
  18. pendingAuthRequests = make(map[string]*Request)
  19. lock = sync.Mutex{}
  20. )
  21. const registrationTimeout = time.Minute * 10
  22. func init() {
  23. go setupExpiredRequestPruner()
  24. }
  25. // Clear out any pending requests that have been pending for greater than
  26. // the specified timeout value.
  27. func setupExpiredRequestPruner() {
  28. pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout)
  29. for range pruneExpiredRequestsTimer.C {
  30. lock.Lock()
  31. log.Debugln("Pruning expired IndieAuth requests.")
  32. for k, v := range pendingAuthRequests {
  33. if time.Since(v.Timestamp) > registrationTimeout {
  34. delete(pendingAuthRequests, k)
  35. }
  36. }
  37. lock.Unlock()
  38. }
  39. }
  40. // StartAuthFlow will begin the IndieAuth flow by generating an auth request.
  41. func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
  42. // Limit the number of pending requests
  43. if len(pendingAuthRequests) >= maxPendingRequests {
  44. return nil, errors.New("Please try again later. Too many pending requests.")
  45. }
  46. // Reject any requests to our internal network or loopback
  47. if utils.IsHostnameInternal(authHost) {
  48. return nil, errors.New("unable to use provided host")
  49. }
  50. // Santity check the server URL
  51. u, err := url.ParseRequestURI(authHost)
  52. if err != nil {
  53. return nil, errors.New("unable to parse server URL")
  54. }
  55. // Limit to only secured connections
  56. if u.Scheme != "https" {
  57. return nil, errors.New("only servers secured with https are supported")
  58. }
  59. serverURL := data.GetServerURL()
  60. if serverURL == "" {
  61. return nil, errors.New("Owncast server URL must be set when using auth")
  62. }
  63. r, err := createAuthRequest(authHost, userID, displayName, accessToken, serverURL)
  64. if err != nil {
  65. return nil, errors.Wrap(err, "unable to generate IndieAuth request")
  66. }
  67. pendingAuthRequests[r.State] = r
  68. return r.Redirect, nil
  69. }
  70. // HandleCallbackCode will handle the callback from the IndieAuth server
  71. // to continue the next step of the auth flow.
  72. func HandleCallbackCode(code, state string) (*Request, *Response, error) {
  73. request, exists := pendingAuthRequests[state]
  74. if !exists {
  75. return nil, nil, errors.New("no auth requests pending")
  76. }
  77. data := url.Values{}
  78. data.Set("grant_type", "authorization_code")
  79. data.Set("code", code)
  80. data.Set("client_id", request.ClientID)
  81. data.Set("redirect_uri", request.Callback.String())
  82. data.Set("code_verifier", request.CodeVerifier)
  83. // Do not support redirects.
  84. client := &http.Client{
  85. CheckRedirect: func(req *http.Request, via []*http.Request) error {
  86. return http.ErrUseLastResponse
  87. },
  88. }
  89. r, err := http.NewRequest("POST", request.Endpoint.String(), strings.NewReader(data.Encode())) // URL-encoded payload
  90. if err != nil {
  91. return nil, nil, err
  92. }
  93. r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  94. r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
  95. res, err := client.Do(r)
  96. if err != nil {
  97. return nil, nil, err
  98. }
  99. defer res.Body.Close()
  100. body, err := io.ReadAll(res.Body)
  101. if err != nil {
  102. return nil, nil, err
  103. }
  104. var response Response
  105. if err := json.Unmarshal(body, &response); err != nil {
  106. return nil, nil, errors.Wrap(err, "unable to parse IndieAuth response: "+string(body))
  107. }
  108. if response.Error != "" || response.ErrorDescription != "" {
  109. errorText := makeIndieAuthClientErrorText(response.Error)
  110. log.Debugln("IndieAuth error:", response.Error, response.ErrorDescription)
  111. return nil, nil, fmt.Errorf("IndieAuth error: %s - %s", errorText, response.ErrorDescription)
  112. }
  113. // In case this IndieAuth server does not use OAuth error keys or has internal
  114. // issues resulting in unstructured errors.
  115. if res.StatusCode < 200 || res.StatusCode > 299 {
  116. log.Debugln("IndieAuth error. status code:", res.StatusCode, "body:", string(body))
  117. return nil, nil, errors.New("there was an error authenticating against IndieAuth server")
  118. }
  119. // Trim any trailing slash so we can accurately compare the two "me" values
  120. meResponseVerifier := strings.TrimRight(response.Me, "/")
  121. meRequestVerifier := strings.TrimRight(request.Me.String(), "/")
  122. // What we sent and what we got back must match
  123. if meRequestVerifier != meResponseVerifier {
  124. return nil, nil, errors.New("indieauth response does not match the initial anticipated auth destination")
  125. }
  126. return request, &response, nil
  127. }
  128. // Error value should be from this list:
  129. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
  130. func makeIndieAuthClientErrorText(err string) string {
  131. switch err {
  132. case "invalid_request", "invalid_client":
  133. return "The authentication request was invalid. Please report this to the Owncast project."
  134. case "invalid_grant", "unauthorized_client":
  135. return "This authorization request is unauthorized."
  136. case "unsupported_grant_type":
  137. return "The authorization grant type is not supported by the authorization server."
  138. default:
  139. return err
  140. }
  141. }