client.go 4.5 KB

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