diff --git a/core/core.go b/core/core.go index 6c2a7a95e..f1dea2a4c 100644 --- a/core/core.go +++ b/core/core.go @@ -7,11 +7,11 @@ import ( log "github.com/sirupsen/logrus" - "github.com/owncast/owncast/auth" "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/auth" "github.com/owncast/owncast/services/notifications" "github.com/owncast/owncast/services/webhooks" "github.com/owncast/owncast/services/yp" diff --git a/auth/auth.go b/services/auth/auth.go similarity index 100% rename from auth/auth.go rename to services/auth/auth.go diff --git a/auth/fediverse/fediverse.go b/services/auth/fediverse/fediverse.go similarity index 57% rename from auth/fediverse/fediverse.go rename to services/auth/fediverse/fediverse.go index b09f429ec..cb27596bf 100644 --- a/auth/fediverse/fediverse.go +++ b/services/auth/fediverse/fediverse.go @@ -11,6 +11,34 @@ import ( log "github.com/sirupsen/logrus" ) +type FediAuth struct { + // Key by access token to limit one OTP request for a person + // to be active at a time. + pendingAuthRequests map[string]OTPRegistration + lock sync.Mutex +} + +var temporaryFediAuthGlobalInstance *FediAuth + +// GetFediAuth returns the temporary global instance. +// Remove this after dependency injection is implemented. +func GetFediAuth() *FediAuth { + if temporaryFediAuthGlobalInstance == nil { + temporaryFediAuthGlobalInstance = NewFediAuth() + } + + return temporaryFediAuthGlobalInstance +} + +// NewFediAuth creates a new FediAuth instance. +func NewFediAuth() *FediAuth { + f := &FediAuth{ + pendingAuthRequests: make(map[string]OTPRegistration), + } + go f.setupExpiredRequestPruner() + return f +} + // OTPRegistration represents a single OTP request. type OTPRegistration struct { Timestamp time.Time @@ -20,43 +48,32 @@ type OTPRegistration struct { Account string } -// Key by access token to limit one OTP request for a person -// to be active at a time. -var ( - pendingAuthRequests = make(map[string]OTPRegistration) - lock = sync.Mutex{} -) - const ( registrationTimeout = time.Minute * 10 maxPendingRequests = 1000 ) -func init() { - go setupExpiredRequestPruner() -} - // Clear out any pending requests that have been pending for greater than // the specified timeout value. -func setupExpiredRequestPruner() { +func (f *FediAuth) setupExpiredRequestPruner() { pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout) for range pruneExpiredRequestsTimer.C { - lock.Lock() + f.lock.Lock() log.Debugln("Pruning expired OTP requests.") - for k, v := range pendingAuthRequests { + for k, v := range f.pendingAuthRequests { if time.Since(v.Timestamp) > registrationTimeout { - delete(pendingAuthRequests, k) + delete(f.pendingAuthRequests, k) } } - lock.Unlock() + f.lock.Unlock() } } // RegisterFediverseOTP will start the OTP flow for a user, creating a new // code and returning it to be sent to a destination. -func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) (OTPRegistration, bool, error) { - request, requestExists := pendingAuthRequests[accessToken] +func (f *FediAuth) RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) (OTPRegistration, bool, error) { + request, requestExists := f.pendingAuthRequests[accessToken] // If a request is already registered and has not expired then return that // existing request. @@ -64,10 +81,10 @@ func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) return request, false, nil } - lock.Lock() - defer lock.Unlock() + f.lock.Lock() + defer f.lock.Unlock() - if len(pendingAuthRequests)+1 > maxPendingRequests { + if len(f.pendingAuthRequests)+1 > maxPendingRequests { return request, false, errors.New("Please try again later. Too many pending requests.") } @@ -79,23 +96,23 @@ func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) Account: strings.ToLower(account), Timestamp: time.Now(), } - pendingAuthRequests[accessToken] = r + f.pendingAuthRequests[accessToken] = r return r, true, nil } // ValidateFediverseOTP will verify a OTP code for a auth request. -func ValidateFediverseOTP(accessToken, code string) (bool, *OTPRegistration) { - request, ok := pendingAuthRequests[accessToken] +func (f *FediAuth) ValidateFediverseOTP(accessToken, code string) (bool, *OTPRegistration) { + request, ok := f.pendingAuthRequests[accessToken] if !ok || request.Code != code || time.Since(request.Timestamp) > registrationTimeout { return false, nil } - lock.Lock() - defer lock.Unlock() + f.lock.Lock() + defer f.lock.Unlock() - delete(pendingAuthRequests, accessToken) + delete(f.pendingAuthRequests, accessToken) return true, &request } diff --git a/auth/fediverse/fediverse_test.go b/services/auth/fediverse/fediverse_test.go similarity index 64% rename from auth/fediverse/fediverse_test.go rename to services/auth/fediverse/fediverse_test.go index c9b3b3550..18139e849 100644 --- a/auth/fediverse/fediverse_test.go +++ b/services/auth/fediverse/fediverse_test.go @@ -14,8 +14,10 @@ const ( userDisplayName = "fake-user-display-name" ) +var fediAuthInstance = NewFediAuth() + func TestOTPFlowValidation(t *testing.T) { - r, success, err := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) + r, success, err := fediAuthInstance.RegisterFediverseOTP(accessToken, userID, userDisplayName, account) if err != nil { t.Error(err) } @@ -36,7 +38,7 @@ func TestOTPFlowValidation(t *testing.T) { t.Error("Timestamp is empty") } - valid, registration := ValidateFediverseOTP(accessToken, r.Code) + valid, registration := fediAuthInstance.ValidateFediverseOTP(accessToken, r.Code) if !valid { t.Error("Code is not valid") } @@ -55,8 +57,8 @@ func TestOTPFlowValidation(t *testing.T) { } func TestSingleOTPFlowRequest(t *testing.T) { - r1, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) - r2, s2, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) + r1, _, _ := fediAuthInstance.RegisterFediverseOTP(accessToken, userID, userDisplayName, account) + r2, s2, _ := fediAuthInstance.RegisterFediverseOTP(accessToken, userID, userDisplayName, account) if r1.Code != r2.Code { t.Error("Only one registration should be permitted.") @@ -70,12 +72,12 @@ func TestSingleOTPFlowRequest(t *testing.T) { func TestAccountCaseInsensitive(t *testing.T) { account := "Account" accessToken := "another-fake-access-token" - r1, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account) - _, reg1 := ValidateFediverseOTP(accessToken, r1.Code) + r1, _, _ := fediAuthInstance.RegisterFediverseOTP(accessToken, userID, userDisplayName, account) + _, reg1 := fediAuthInstance.ValidateFediverseOTP(accessToken, r1.Code) // Simulate second auth with account in different case - r2, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, strings.ToUpper(account)) - _, reg2 := ValidateFediverseOTP(accessToken, r2.Code) + r2, _, _ := fediAuthInstance.RegisterFediverseOTP(accessToken, userID, userDisplayName, strings.ToUpper(account)) + _, reg2 := fediAuthInstance.ValidateFediverseOTP(accessToken, r2.Code) if reg1.Account != reg2.Account { t.Errorf("Account names should be case-insensitive: %s %s", reg1.Account, reg2.Account) @@ -88,9 +90,9 @@ func TestLimitGlobalPendingRequests(t *testing.T) { uid, _ := utils.GenerateRandomString(10) account, _ := utils.GenerateRandomString(10) - _, success, error := RegisterFediverseOTP(at, uid, "userDisplayName", account) + _, success, error := fediAuthInstance.RegisterFediverseOTP(at, uid, "userDisplayName", account) if !success { - t.Error("Registration should be permitted.", i, " of ", len(pendingAuthRequests)) + t.Error("Registration should be permitted.", i, " of ", len(fediAuthInstance.pendingAuthRequests)) } if error != nil { t.Error(error) @@ -101,7 +103,7 @@ func TestLimitGlobalPendingRequests(t *testing.T) { at, _ := utils.GenerateRandomString(10) uid, _ := utils.GenerateRandomString(10) account, _ := utils.GenerateRandomString(10) - _, success, error := RegisterFediverseOTP(at, uid, "userDisplayName", account) + _, success, error := fediAuthInstance.RegisterFediverseOTP(at, uid, "userDisplayName", account) if success { t.Error("Registration should not be permitted.") } diff --git a/auth/indieauth/client.go b/services/auth/indieauth/client.go similarity index 87% rename from auth/indieauth/client.go rename to services/auth/indieauth/client.go index 9d7a4e736..aa722b114 100644 --- a/auth/indieauth/client.go +++ b/services/auth/indieauth/client.go @@ -8,7 +8,6 @@ import ( "net/url" "strconv" "strings" - "sync" "time" "github.com/owncast/owncast/core/data" @@ -17,38 +16,28 @@ import ( 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() { +func (c *IndieAuthClient) setupExpiredRequestPruner() { pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout) for range pruneExpiredRequestsTimer.C { - lock.Lock() + c.lock.Lock() log.Debugln("Pruning expired IndieAuth requests.") - for k, v := range pendingAuthRequests { + for k, v := range c.pendingAuthRequests { if time.Since(v.Timestamp) > registrationTimeout { - delete(pendingAuthRequests, k) + delete(c.pendingAuthRequests, k) } } - lock.Unlock() + c.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 { +func (c *IndieAuthClient) StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) { + if len(c.pendingAuthRequests) >= maxPendingRequests { return nil, errors.New("Please try again later. Too many pending requests.") } @@ -78,15 +67,15 @@ func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, return nil, errors.Wrap(err, "unable to generate IndieAuth request") } - pendingAuthRequests[r.State] = r + c.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] +func (c *IndieAuthClient) HandleCallbackCode(code, state string) (*Request, *Response, error) { + request, exists := c.pendingAuthRequests[state] if !exists { return nil, nil, errors.New("no auth requests pending") } diff --git a/auth/indieauth/helpers.go b/services/auth/indieauth/helpers.go similarity index 100% rename from auth/indieauth/helpers.go rename to services/auth/indieauth/helpers.go diff --git a/services/auth/indieauth/indieauth.go b/services/auth/indieauth/indieauth.go new file mode 100644 index 000000000..def72ea5d --- /dev/null +++ b/services/auth/indieauth/indieauth.go @@ -0,0 +1,55 @@ +package indieauth + +import "sync" + +type IndieAuthClient struct { + pendingAuthRequests map[string]*Request + lock sync.Mutex +} + +type IndieAuthServer struct { + pendingServerAuthRequests map[string]ServerAuthRequest +} + +var temporaryGlobalClientInstance *IndieAuthClient + +// GetIndieAuthClient returns the temporary global instance of IndieAuthClient. +// Remove this after dependency injection is implemented. +func GetIndieAuthClient() *IndieAuthClient { + if temporaryGlobalClientInstance == nil { + temporaryGlobalClientInstance = NewIndieAuthClient() + } + + return temporaryGlobalClientInstance +} + +// NewIndieAuthClient creates a new IndieAuth client instance. +func NewIndieAuthClient() *IndieAuthClient { + i := &IndieAuthClient{ + pendingAuthRequests: make(map[string]*Request), + } + go i.setupExpiredRequestPruner() + + return i +} + +var temporaryGlobalServerInstance *IndieAuthServer + +// GetIndieAuthServer returns the temporary global instance of IndieAuthServer. +// Remove this after dependency injection is implemented. +func GetIndieAuthServer() *IndieAuthServer { + if temporaryGlobalServerInstance == nil { + temporaryGlobalServerInstance = NewIndieAuthServer() + } + + return temporaryGlobalServerInstance +} + +// NewIndieAuthServer creates a new IndieAuth client instance. +func NewIndieAuthServer() *IndieAuthServer { + i := &IndieAuthServer{ + pendingServerAuthRequests: make(map[string]ServerAuthRequest), + } + + return i +} diff --git a/auth/indieauth/indieauth_test.go b/services/auth/indieauth/indieauth_test.go similarity index 71% rename from auth/indieauth/indieauth_test.go rename to services/auth/indieauth/indieauth_test.go index 20c5b5970..661f05f1c 100644 --- a/auth/indieauth/indieauth_test.go +++ b/services/auth/indieauth/indieauth_test.go @@ -6,6 +6,8 @@ import ( "github.com/owncast/owncast/utils" ) +var indieAuthServer = GetIndieAuthServer() + func TestLimitGlobalPendingRequests(t *testing.T) { // Simulate 10 pending requests for i := 0; i < maxPendingRequests-1; i++ { @@ -15,9 +17,9 @@ func TestLimitGlobalPendingRequests(t *testing.T) { state, _ := utils.GenerateRandomString(10) me, _ := utils.GenerateRandomString(10) - _, err := StartServerAuth(cid, redirectURL, cc, state, me) + _, err := indieAuthServer.StartServerAuth(cid, redirectURL, cc, state, me) if err != nil { - t.Error("Registration should be permitted.", i, " of ", len(pendingAuthRequests), err) + t.Error("Registration should be permitted.", i, " of ", len(indieAuthServer.pendingServerAuthRequests), err) } } @@ -28,7 +30,7 @@ func TestLimitGlobalPendingRequests(t *testing.T) { state, _ := utils.GenerateRandomString(10) me, _ := utils.GenerateRandomString(10) - _, err := StartServerAuth(cid, redirectURL, cc, state, me) + _, err := indieAuthServer.StartServerAuth(cid, redirectURL, cc, state, me) if err == nil { t.Error("Registration should not be permitted.") } diff --git a/auth/indieauth/random.go b/services/auth/indieauth/random.go similarity index 100% rename from auth/indieauth/random.go rename to services/auth/indieauth/random.go diff --git a/auth/indieauth/request.go b/services/auth/indieauth/request.go similarity index 100% rename from auth/indieauth/request.go rename to services/auth/indieauth/request.go diff --git a/auth/indieauth/response.go b/services/auth/indieauth/response.go similarity index 100% rename from auth/indieauth/response.go rename to services/auth/indieauth/response.go diff --git a/auth/indieauth/server.go b/services/auth/indieauth/server.go similarity index 84% rename from auth/indieauth/server.go rename to services/auth/indieauth/server.go index 0d2ddb378..640336ce4 100644 --- a/auth/indieauth/server.go +++ b/services/auth/indieauth/server.go @@ -38,15 +38,13 @@ type ServerProfileResponse struct { ErrorDescription string `json:"error_description,omitempty"` } -var pendingServerAuthRequests = map[string]ServerAuthRequest{} - const maxPendingRequests = 100 // StartServerAuth will handle the authentication for the admin user of this // Owncast server. Initiated via a GET of the auth endpoint. // https://indieweb.org/authorization-endpoint -func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*ServerAuthRequest, error) { - if len(pendingServerAuthRequests)+1 >= maxPendingRequests { +func (s *IndieAuthServer) StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*ServerAuthRequest, error) { + if len(s.pendingServerAuthRequests)+1 >= maxPendingRequests { return nil, errors.New("Please try again later. Too many pending requests.") } @@ -62,15 +60,15 @@ func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*S Timestamp: time.Now(), } - pendingServerAuthRequests[code] = r + s.pendingServerAuthRequests[code] = r return &r, nil } // CompleteServerAuth will verify that the values provided in the final step // of the IndieAuth flow are correct, and return some basic profile info. -func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) { - request, pending := pendingServerAuthRequests[code] +func (s *IndieAuthServer) CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) { + request, pending := s.pendingServerAuthRequests[code] if !pending { return nil, errors.New("no pending authentication request") } diff --git a/auth/persistence.go b/services/auth/persistence.go similarity index 100% rename from auth/persistence.go rename to services/auth/persistence.go diff --git a/webserver/handlers/auth/fediverse/fediverse.go b/webserver/handlers/auth/fediverse/fediverse.go index 2e2df52dd..dbae56946 100644 --- a/webserver/handlers/auth/fediverse/fediverse.go +++ b/webserver/handlers/auth/fediverse/fediverse.go @@ -6,11 +6,12 @@ import ( "net/http" "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/auth" - fediverseauth "github.com/owncast/owncast/auth/fediverse" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/auth" + fediverseauth "github.com/owncast/owncast/services/auth/fediverse" + "github.com/owncast/owncast/storage" "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -27,8 +28,9 @@ func RegisterFediverseOTPRequest(u user.User, w http.ResponseWriter, r *http.Req return } + fediAuth := fediverseauth.GetFediAuth() accessToken := r.URL.Query().Get("accessToken") - reg, success, err := fediverseauth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount) + reg, success, err := fediAuth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount) if err != nil { responses.WriteSimpleResponse(w, false, "Could not register auth request: "+err.Error()) return @@ -61,7 +63,9 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { return } accessToken := r.URL.Query().Get("accessToken") - valid, authRegistration := fediverseauth.ValidateFediverseOTP(accessToken, req.Code) + fediAuth := fediverseauth.GetFediAuth() + + valid, authRegistration := fediAuth.ValidateFediverseOTP(accessToken, req.Code) if !valid { w.WriteHeader(http.StatusForbidden) return diff --git a/webserver/handlers/auth/indieauth/client.go b/webserver/handlers/auth/indieauth/client.go index 5f3ac8b4b..f37eb1dd6 100644 --- a/webserver/handlers/auth/indieauth/client.go +++ b/webserver/handlers/auth/indieauth/client.go @@ -6,10 +6,11 @@ import ( "io" "net/http" - "github.com/owncast/owncast/auth" - ia "github.com/owncast/owncast/auth/indieauth" "github.com/owncast/owncast/core/chat" - "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/auth" + ia "github.com/owncast/owncast/services/auth/indieauth" + "github.com/owncast/owncast/storage" "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -38,7 +39,8 @@ func StartAuthFlow(u user.User, w http.ResponseWriter, r *http.Request) { accessToken := r.URL.Query().Get("accessToken") - redirectURL, err := ia.StartAuthFlow(authRequest.AuthHost, u.ID, accessToken, u.DisplayName) + indieAuthClient := ia.GetIndieAuthClient() + redirectURL, err := indieAuthClient.StartAuthFlow(authRequest.AuthHost, u.ID, accessToken, u.DisplayName) if err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return @@ -53,9 +55,10 @@ func StartAuthFlow(u user.User, w http.ResponseWriter, r *http.Request) { // HandleRedirect will handle the redirect from an IndieAuth server to // continue the auth flow. func HandleRedirect(w http.ResponseWriter, r *http.Request) { + indieAuthClient := ia.GetIndieAuthClient() state := r.URL.Query().Get("state") code := r.URL.Query().Get("code") - request, response, err := ia.HandleCallbackCode(code, state) + request, response, err := indieAuthClient.HandleCallbackCode(code, state) if err != nil { log.Debugln(err) msg := `Unable to complete authentication. Go back.
` diff --git a/webserver/handlers/auth/indieauth/server.go b/webserver/handlers/auth/indieauth/server.go index 58ee82793..9396bea5a 100644 --- a/webserver/handlers/auth/indieauth/server.go +++ b/webserver/handlers/auth/indieauth/server.go @@ -4,7 +4,7 @@ import ( "net/http" "net/url" - ia "github.com/owncast/owncast/auth/indieauth" + ia "github.com/owncast/owncast/services/auth/indieauth" "github.com/owncast/owncast/webserver/middleware" "github.com/owncast/owncast/webserver/responses" ) @@ -31,7 +31,8 @@ func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { state := r.URL.Query().Get("state") me := r.URL.Query().Get("me") - request, err := ia.StartServerAuth(clientID, redirectURI, codeChallenge, state, me) + indieAuthServer := ia.GetIndieAuthServer() + request, err := indieAuthServer.StartServerAuth(clientID, redirectURI, codeChallenge, state, me) if err != nil { _ = responses.WriteString(w, err.Error(), http.StatusInternalServerError) return @@ -70,7 +71,8 @@ func handleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { // If the server auth flow cannot be completed then return with specific // "invalid_client" error. - response, err := ia.CompleteServerAuth(code, redirectURI, clientID, codeVerifier) + indieAuthServer := ia.GetIndieAuthServer() + response, err := indieAuthServer.CompleteServerAuth(code, redirectURI, clientID, codeVerifier) if err != nil { responses.WriteResponse(w, ia.Response{ Error: "invalid_client",