123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- package indieauth
- import (
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/owncast/owncast/core/data"
- "github.com/owncast/owncast/utils"
- "github.com/pkg/errors"
- log "github.com/sirupsen/logrus"
- )
- var (
- pendingAuthRequests = make(map[string]*Request)
- lock = sync.Mutex{}
- )
- const registrationTimeout = time.Minute * 10
- func init() {
- go setupExpiredRequestPruner()
- }
- // Clear out any pending requests that have been pending for greater than
- // the specified timeout value.
- func setupExpiredRequestPruner() {
- pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout)
- for range pruneExpiredRequestsTimer.C {
- lock.Lock()
- log.Debugln("Pruning expired IndieAuth requests.")
- for k, v := range pendingAuthRequests {
- if time.Since(v.Timestamp) > registrationTimeout {
- delete(pendingAuthRequests, k)
- }
- }
- lock.Unlock()
- }
- }
- // StartAuthFlow will begin the IndieAuth flow by generating an auth request.
- func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
- // Limit the number of pending requests
- if len(pendingAuthRequests) >= maxPendingRequests {
- return nil, errors.New("Please try again later. Too many pending requests.")
- }
- // Reject any requests to our internal network or loopback
- if utils.IsHostnameInternal(authHost) {
- return nil, errors.New("unable to use provided host")
- }
- // Santity check the server URL
- u, err := url.ParseRequestURI(authHost)
- if err != nil {
- return nil, errors.New("unable to parse server URL")
- }
- // Limit to only secured connections
- if u.Scheme != "https" {
- return nil, errors.New("only servers secured with https are supported")
- }
- serverURL := data.GetServerURL()
- if serverURL == "" {
- return nil, errors.New("Owncast server URL must be set when using auth")
- }
- r, err := createAuthRequest(authHost, userID, displayName, accessToken, serverURL)
- if err != nil {
- return nil, errors.Wrap(err, "unable to generate IndieAuth request")
- }
- pendingAuthRequests[r.State] = r
- return r.Redirect, nil
- }
- // HandleCallbackCode will handle the callback from the IndieAuth server
- // to continue the next step of the auth flow.
- func HandleCallbackCode(code, state string) (*Request, *Response, error) {
- request, exists := pendingAuthRequests[state]
- if !exists {
- return nil, nil, errors.New("no auth requests pending")
- }
- data := url.Values{}
- data.Set("grant_type", "authorization_code")
- data.Set("code", code)
- data.Set("client_id", request.ClientID)
- data.Set("redirect_uri", request.Callback.String())
- data.Set("code_verifier", request.CodeVerifier)
- // Do not support redirects.
- client := &http.Client{
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- },
- }
- r, err := http.NewRequest("POST", request.Endpoint.String(), strings.NewReader(data.Encode())) // URL-encoded payload
- if err != nil {
- return nil, nil, err
- }
- r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
- res, err := client.Do(r)
- if err != nil {
- return nil, nil, err
- }
- defer res.Body.Close()
- body, err := io.ReadAll(res.Body)
- if err != nil {
- return nil, nil, err
- }
- var response Response
- if err := json.Unmarshal(body, &response); err != nil {
- return nil, nil, errors.Wrap(err, "unable to parse IndieAuth response: "+string(body))
- }
- if response.Error != "" || response.ErrorDescription != "" {
- errorText := makeIndieAuthClientErrorText(response.Error)
- log.Debugln("IndieAuth error:", response.Error, response.ErrorDescription)
- return nil, nil, fmt.Errorf("IndieAuth error: %s - %s", errorText, response.ErrorDescription)
- }
- // In case this IndieAuth server does not use OAuth error keys or has internal
- // issues resulting in unstructured errors.
- if res.StatusCode < 200 || res.StatusCode > 299 {
- log.Debugln("IndieAuth error. status code:", res.StatusCode, "body:", string(body))
- return nil, nil, errors.New("there was an error authenticating against IndieAuth server")
- }
- // Trim any trailing slash so we can accurately compare the two "me" values
- meResponseVerifier := strings.TrimRight(response.Me, "/")
- meRequestVerifier := strings.TrimRight(request.Me.String(), "/")
- // What we sent and what we got back must match
- if meRequestVerifier != meResponseVerifier {
- return nil, nil, errors.New("indieauth response does not match the initial anticipated auth destination")
- }
- return request, &response, nil
- }
- // Error value should be from this list:
- // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
- func makeIndieAuthClientErrorText(err string) string {
- switch err {
- case "invalid_request", "invalid_client":
- return "The authentication request was invalid. Please report this to the Owncast project."
- case "invalid_grant", "unauthorized_client":
- return "This authorization request is unauthorized."
- case "unsupported_grant_type":
- return "The authorization grant type is not supported by the authorization server."
- default:
- return err
- }
- }
|