diff --git a/internal/asyncutil/asyncutil.go b/internal/asyncutil/asyncutil.go new file mode 100644 index 00000000..bc9aec5a --- /dev/null +++ b/internal/asyncutil/asyncutil.go @@ -0,0 +1,42 @@ +// Package asyncutil tracks detached, fire-and-forget goroutines (email/SMS +// sends, webhook events, audit logs) that request handlers start and don't +// wait on. Go recovers panics so a background side-effect can never take +// down the process; Wait lets graceful shutdown drain them before exiting. +package asyncutil + +import ( + "sync/atomic" + "time" + + "github.com/rs/zerolog" +) + +var counter atomic.Int64 + +// Go runs fn in a new goroutine, tracking it so Wait can block until it +// finishes and recovering any panic fn raises. log may be nil, in which +// case a recovered panic is dropped (all current call sites pass a logger). +func Go(log *zerolog.Logger, fn func()) { + counter.Add(1) + go func() { + defer counter.Add(-1) + defer func() { + if r := recover(); r != nil && log != nil { + log.Error().Interface("panic", r).Msg("recovered panic in background goroutine") + } + }() + fn() + }() +} + +// Wait blocks until every goroutine started via Go has finished. Intended +// for use once during graceful shutdown, after listeners have stopped +// accepting new requests. +func Wait(log zerolog.Logger) { + if n := counter.Load(); n > 0 { + log.Info().Int64("active_goroutines", n).Msg("waiting for background goroutines to complete") + } + for counter.Load() > 0 { + time.Sleep(100 * time.Millisecond) + } +} diff --git a/internal/asyncutil/asyncutil_test.go b/internal/asyncutil/asyncutil_test.go new file mode 100644 index 00000000..bbab34b9 --- /dev/null +++ b/internal/asyncutil/asyncutil_test.go @@ -0,0 +1,29 @@ +package asyncutil + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/rs/zerolog" +) + +func TestGoWaitRunsAndDrains(t *testing.T) { + var ran atomic.Bool + Go(nil, func() { + time.Sleep(20 * time.Millisecond) + ran.Store(true) + }) + Wait(zerolog.Nop()) + if !ran.Load() { + t.Fatal("Wait returned before the goroutine finished") + } +} + +func TestGoRecoversPanic(t *testing.T) { + log := zerolog.Nop() + Go(&log, func() { + panic("boom") + }) + Wait(log) // must not propagate the panic to the test goroutine +} diff --git a/internal/audit/provider.go b/internal/audit/provider.go index 688c3b21..941ff0a8 100644 --- a/internal/audit/provider.go +++ b/internal/audit/provider.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/storage" "github.com/authorizerdev/authorizer/internal/storage/schemas" ) @@ -85,7 +86,7 @@ func metadataWithProtocol(meta, protocol string) string { // LogEvent asynchronously records an audit log entry. func (p *provider) LogEvent(event Event) { - go func() { + asyncutil.Go(p.deps.Log, func() { log := p.deps.Log.With().Str("func", "LogEvent").Logger() auditLog := &schemas.AuditLog{ ActorID: event.ActorID, @@ -101,5 +102,5 @@ func (p *provider) LogEvent(event Event) { if err := p.deps.StorageProvider.AddAuditLog(context.Background(), auditLog); err != nil { log.Debug().Err(err).Str("action", event.Action).Msg("Failed to add audit log") } - }() + }) } diff --git a/internal/http_handlers/logout.go b/internal/http_handlers/logout.go index f05386c3..8960f2e0 100644 --- a/internal/http_handlers/logout.go +++ b/internal/http_handlers/logout.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -133,7 +134,8 @@ func (h *httpProvider) LogoutHandler() gin.HandlerFunc { // logout response is never blocked by a slow receiver. if strings.TrimSpace(h.Config.BackchannelLogoutURI) != "" { hostname := parsers.GetHost(gc) - go func(uri, host, sub, sid string) { + uri, host, sub, sid := h.Config.BackchannelLogoutURI, hostname, userID, sessionData.Nonce + asyncutil.Go(h.Log, func() { if err := h.TokenProvider.NotifyBackchannelLogout(context.Background(), uri, &token.BackchannelLogoutConfig{ HostName: host, Subject: sub, @@ -141,7 +143,7 @@ func (h *httpProvider) LogoutHandler() gin.HandlerFunc { }); err != nil { log.Debug().Err(err).Msg("backchannel logout notification failed") } - }(h.Config.BackchannelLogoutURI, hostname, userID, sessionData.Nonce) + }) } if redirectURL != "" { diff --git a/internal/http_handlers/oauth_callback.go b/internal/http_handlers/oauth_callback.go index 430aa6ca..9026a5aa 100644 --- a/internal/http_handlers/oauth_callback.go +++ b/internal/http_handlers/oauth_callback.go @@ -18,6 +18,7 @@ import ( "github.com/google/uuid" goredis "github.com/redis/go-redis/v9" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -398,7 +399,7 @@ func (h *httpProvider) OAuthCallbackHandler() gin.HandlerFunc { bgCtx := context.WithoutCancel(ctx) userAgent := utils.GetUserAgent(ctx.Request) ip := utils.GetIP(ctx.Request) - go func() { + asyncutil.Go(h.Log, func() { if isSignUp { _ = h.EventsProvider.RegisterEvent(bgCtx, constants.UserSignUpWebhookEvent, provider, user) // User is also logged in with signup @@ -413,7 +414,7 @@ func (h *httpProvider) OAuthCallbackHandler() gin.HandlerFunc { }); err != nil { log.Debug().Err(err).Msg("Failed to add session") } - }() + }) if strings.Contains(redirectURL, "?") { redirectURL = redirectURL + "&" + params } else { diff --git a/internal/http_handlers/oauth_sso.go b/internal/http_handlers/oauth_sso.go index cd3a05cc..1710d756 100644 --- a/internal/http_handlers/oauth_sso.go +++ b/internal/http_handlers/oauth_sso.go @@ -44,6 +44,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/golang-jwt/jwt/v4" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -661,7 +662,7 @@ func (h *httpProvider) issueSSOSession(c *gin.Context, flow *ssoFlowState, user bgCtx := context.WithoutCancel(c.Request.Context()) userAgent := utils.GetUserAgent(c.Request) ip := utils.GetIP(c.Request) - go func() { + asyncutil.Go(h.Log, func() { if isSignUp { _ = h.EventsProvider.RegisterEvent(bgCtx, constants.UserSignUpWebhookEvent, constants.AuthRecipeMethodSSO, user) } @@ -669,7 +670,7 @@ func (h *httpProvider) issueSSOSession(c *gin.Context, flow *ssoFlowState, user if err := h.StorageProvider.AddSession(bgCtx, &schemas.Session{UserID: user.ID, UserAgent: userAgent, IP: ip}); err != nil { h.Log.Debug().Err(err).Msg("failed to add session") } - }() + }) params := "state=" + url.QueryEscape(flow.AppState) redirectURL := flow.AppRedirect diff --git a/internal/http_handlers/saml_sp.go b/internal/http_handlers/saml_sp.go index 36ecf1d0..f1c57a5e 100644 --- a/internal/http_handlers/saml_sp.go +++ b/internal/http_handlers/saml_sp.go @@ -54,6 +54,7 @@ import ( goredis "github.com/redis/go-redis/v9" "github.com/rs/zerolog" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -543,7 +544,7 @@ func (h *httpProvider) issueSAMLSession(c *gin.Context, slug, orgID, appRedirect bgCtx := context.WithoutCancel(c.Request.Context()) userAgent := utils.GetUserAgent(c.Request) ip := utils.GetIP(c.Request) - go func() { + asyncutil.Go(h.Log, func() { if isSignUp { _ = h.EventsProvider.RegisterEvent(bgCtx, constants.UserSignUpWebhookEvent, constants.AuthRecipeMethodSSO, user) } @@ -551,7 +552,7 @@ func (h *httpProvider) issueSAMLSession(c *gin.Context, slug, orgID, appRedirect if err := h.StorageProvider.AddSession(bgCtx, &schemas.Session{UserID: user.ID, UserAgent: userAgent, IP: ip}); err != nil { h.Log.Debug().Err(err).Msg("failed to add session") } - }() + }) params := "state=" + url.QueryEscape(appState) redirectURL := appRedirect diff --git a/internal/http_handlers/verify_email.go b/internal/http_handlers/verify_email.go index 2cd0125c..4d80d176 100644 --- a/internal/http_handlers/verify_email.go +++ b/internal/http_handlers/verify_email.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -192,7 +193,7 @@ func (h *httpProvider) VerifyEmailHandler() gin.HandlerFunc { } else { nonce = authorizeState } - go func() { _ = h.MemoryStoreProvider.RemoveState(state) }() + asyncutil.Go(h.Log, func() { _ = h.MemoryStoreProvider.RemoveState(state) }) } } if nonce == "" { @@ -267,7 +268,7 @@ func (h *httpProvider) VerifyEmailHandler() gin.HandlerFunc { bgCtx := context.WithoutCancel(c) userAgent := utils.GetUserAgent(c.Request) ip := utils.GetIP(c.Request) - go func() { + asyncutil.Go(h.Log, func() { if isSignUp { _ = h.EventsProvider.RegisterEvent(bgCtx, constants.UserSignUpWebhookEvent, loginMethod, user) // User is also logged in with signup @@ -282,7 +283,7 @@ func (h *httpProvider) VerifyEmailHandler() gin.HandlerFunc { }); err != nil { log.Debug().Err(err).Msg("Error adding session") } - }() + }) c.Redirect(http.StatusTemporaryRedirect, redirectURL) } diff --git a/internal/server/server.go b/internal/server/server.go index 437591ed..e54ba157 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/config" "github.com/authorizerdev/authorizer/internal/gateway" "github.com/authorizerdev/authorizer/internal/graphql" @@ -148,6 +149,9 @@ func (s *server) Run(ctx context.Context) error { if err := metricsSrv.Shutdown(shCtx); err != nil { s.Dependencies.Log.Error().Err(err).Msg("Metrics server graceful shutdown failed") } + // Drain fire-and-forget background work (emails, webhooks, audit logs) + // spawned by in-flight requests before the process exits. + asyncutil.Wait(*s.Dependencies.Log) return nil } diff --git a/internal/service/admin_access.go b/internal/service/admin_access.go index cffc85df..14164470 100644 --- a/internal/service/admin_access.go +++ b/internal/service/admin_access.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/graph/model" @@ -45,11 +46,11 @@ func (p *provider) RevokeAccess(ctx context.Context, meta RequestMetadata, param return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserAccessRevokedWebhookEvent, "", user) - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditAdminAccessRevokedEvent, Protocol: meta.Protocol, ActorType: constants.AuditActorTypeAdmin, @@ -92,10 +93,10 @@ func (p *provider) EnableAccess(ctx context.Context, meta RequestMetadata, param log.Debug().Err(err).Msg("Failed to update user") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserAccessEnabledWebhookEvent, "", user) - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditAdminAccessEnabledEvent, Protocol: meta.Protocol, ActorType: constants.AuditActorTypeAdmin, @@ -239,13 +240,13 @@ func (p *provider) InviteMembers(ctx context.Context, meta RequestMetadata, para } // exec it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { _ = p.EmailProvider.SendEmail([]string{refs.StringValue(user.Email)}, constants.VerificationTypeInviteMember, map[string]interface{}{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), "verification_url": utils.GetInviteVerificationURL(verifyEmailURL, verificationToken, redirectURL), }) - }() + }) } invitedUsers := []*model.User{} diff --git a/internal/service/admin_users.go b/internal/service/admin_users.go index 910e7c3f..ca5486dc 100644 --- a/internal/service/admin_users.go +++ b/internal/service/admin_users.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/graph/model" @@ -224,7 +225,7 @@ func (p *provider) UpdateUser(ctx context.Context, meta RequestMetadata, params return nil, nil, fmt.Errorf("user with this email address already exists") } - go func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }() + asyncutil.Go(p.Log, func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }) // gin-shim: parsers.GetHost / GetAppURL still take a *gin.Context. gc := &gin.Context{Request: meta.Request} @@ -264,13 +265,13 @@ func (p *provider) UpdateUser(ctx context.Context, meta RequestMetadata, params } // exec it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { _ = p.EmailProvider.SendEmail([]string{refs.StringValue(user.Email)}, constants.VerificationTypeBasicAuthSignup, map[string]interface{}{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), "verification_url": utils.GetEmailVerificationURL(verificationToken, hostname, redirectURL), }) - }() + }) } if params.PhoneNumber != nil && refs.StringValue(user.PhoneNumber) != refs.StringValue(params.PhoneNumber) { @@ -286,7 +287,7 @@ func (p *provider) UpdateUser(ctx context.Context, meta RequestMetadata, params log.Debug().Str("phone", phone).Msg("User with phone number already exists") return nil, nil, fmt.Errorf("user with this phone number already exists") } - go func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }() + asyncutil.Go(p.Log, func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }) user.PhoneNumber = &phone user.PhoneNumberVerifiedAt = nil } @@ -311,7 +312,7 @@ func (p *provider) UpdateUser(ctx context.Context, meta RequestMetadata, params rolesToSave = strings.Join(inputRoles, ",") } - go func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }() + asyncutil.Go(p.Log, func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }) } if rolesToSave != "" { @@ -381,7 +382,7 @@ func (p *provider) DeleteUser(ctx context.Context, meta RequestMetadata, params Message: `user deleted successfully`, } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) // delete otp for given email otp, err := p.StorageProvider.GetOTPByEmail(ctx, refs.StringValue(user.Email)) @@ -426,7 +427,7 @@ func (p *provider) DeleteUser(ctx context.Context, meta RequestMetadata, params _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserDeletedWebhookEvent, "", user) - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditAdminUserDeletedEvent, Protocol: meta.Protocol, ActorType: constants.AuditActorTypeAdmin, diff --git a/internal/service/auth_response.go b/internal/service/auth_response.go index 8f36dfa9..29db2233 100644 --- a/internal/service/auth_response.go +++ b/internal/service/auth_response.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" "github.com/authorizerdev/authorizer/internal/graph/model" @@ -87,7 +88,7 @@ func (p *provider) issueAuthResponse(ctx context.Context, meta RequestMetadata, } } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) if isSignUp { _ = p.EventsProvider.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) @@ -104,7 +105,7 @@ func (p *provider) issueAuthResponse(ctx context.Context, meta RequestMetadata, }); err != nil { log.Debug().Err(err).Msg("Failed to add session") } - }() + }) authTokenExpiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix() if authTokenExpiresIn <= 0 { diff --git a/internal/service/deactivate_account.go b/internal/service/deactivate_account.go index 0b5fe406..057e2132 100644 --- a/internal/service/deactivate_account.go +++ b/internal/service/deactivate_account.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/graph/model" @@ -39,11 +40,11 @@ func (p *provider) DeactivateAccount(ctx context.Context, meta RequestMetadata) log.Debug().Err(err).Msg("Failed to update user") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserDeactivatedWebhookEvent, "", user) - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditUserDeactivatedEvent, Protocol: meta.Protocol, ActorID: user.ID, diff --git a/internal/service/forgot_password.go b/internal/service/forgot_password.go index 64b34a28..801d2309 100644 --- a/internal/service/forgot_password.go +++ b/internal/service/forgot_password.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -121,13 +122,13 @@ func (p *provider) ForgotPassword(ctx context.Context, meta RequestMetadata, par return nil, nil, err } // execute it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { _ = p.EmailProvider.SendEmail([]string{email}, constants.VerificationTypeForgotPassword, map[string]any{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), "verification_url": utils.GetForgotPasswordURL(verificationToken, redirectURI), }) - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditForgotPasswordEvent, Protocol: meta.Protocol, ActorID: user.ID, diff --git a/internal/service/login.go b/internal/service/login.go index 10fb0023..fc34ef62 100644 --- a/internal/service/login.go +++ b/internal/service/login.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "golang.org/x/crypto/bcrypt" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -196,7 +197,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode log.Debug().Msg("Failed to set mfa session") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) // exec it as go routine so that we can reduce the api latency if err := p.EmailProvider.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]any{ @@ -207,7 +208,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode log.Debug().Msg("Failed to send otp email") } _ = p.EventsProvider.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) - }() + }) return &model.AuthResponse{ Message: "Please check email inbox for the OTP", ShouldShowEmailOtpScreen: refs.NewBoolRef(isEmailLogin), @@ -237,7 +238,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode log.Debug().Msg("Failed to set mfa session") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) smsBody := strings.Builder{} smsBody.WriteString("Your verification code is: ") @@ -246,7 +247,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode if err := p.SMSProvider.SendSMS(phoneNumber, smsBody.String()); err != nil { log.Debug().Msg("Failed to send sms") } - }() + }) return &model.AuthResponse{ Message: "Please check text message for the OTP", ShouldShowMobileOtpScreen: refs.NewBoolRef(isMobileLogin), @@ -336,7 +337,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode log.Debug().Msg("Failed to set mfa session") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) // exec it as go routine so that we can reduce the api latency if err := p.EmailProvider.SendEmail([]string{refs.StringValue(user.Email)}, constants.VerificationTypeOTP, map[string]any{ @@ -347,7 +348,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode log.Debug().Msg("Failed to send otp email") } _ = p.EventsProvider.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) - }() + }) res := &model.AuthResponse{ Message: "Please check email inbox for the OTP", ShouldShowEmailOtpScreen: refs.NewBoolRef(true), @@ -374,7 +375,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode log.Debug().Msg("Failed to set mfa session") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) smsBody := strings.Builder{} smsBody.WriteString("Your verification code is: ") @@ -383,7 +384,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode if err := p.SMSProvider.SendSMS(refs.StringValue(user.PhoneNumber), smsBody.String()); err != nil { log.Debug().Msg("Failed to send sms") } - }() + }) res := &model.AuthResponse{ Message: "Please check text message for the OTP", ShouldShowMobileOtpScreen: refs.NewBoolRef(true), @@ -575,7 +576,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode _ = p.MemoryStoreProvider.SetUserSession(sessionStoreKey, constants.TokenTypeRefreshToken+"_"+authToken.FingerPrint, authToken.RefreshToken.Token, authToken.RefreshToken.ExpiresAt) } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) // Register event if isEmailLogin { @@ -589,7 +590,7 @@ func (p *provider) Login(ctx context.Context, meta RequestMetadata, params *mode UserAgent: meta.UserAgent, IP: meta.IPAddress, }) - }() + }) metrics.RecordAuthEvent(metrics.EventLogin, metrics.StatusSuccess) metrics.ActiveSessions.Inc() p.AuditProvider.LogEvent(audit.Event{ diff --git a/internal/service/magic_link_login.go b/internal/service/magic_link_login.go index 1aa1eb97..bec6b31c 100644 --- a/internal/service/magic_link_login.go +++ b/internal/service/magic_link_login.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/graph/model" @@ -73,10 +74,10 @@ func (p *provider) MagicLinkLogin(ctx context.Context, meta RequestMetadata, par user.Roles = strings.Join(inputRoles, ",") user, _ = p.StorageProvider.AddUser(ctx, user) - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodMagicLinkLogin, user) - }() + }) } else { user = existingUser // There multiple scenarios with roles here in magic link login @@ -197,13 +198,13 @@ func (p *provider) MagicLinkLogin(ctx context.Context, meta RequestMetadata, par } // exec it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { _ = p.EmailProvider.SendEmail([]string{params.Email}, constants.VerificationTypeMagicLinkLogin, map[string]any{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), "verification_url": utils.GetEmailVerificationURL(verificationToken, hostname, redirectURL), }) - }() + }) } p.AuditProvider.LogEvent(audit.Event{ diff --git a/internal/service/otp_mfa_setup.go b/internal/service/otp_mfa_setup.go index c774f175..0721dcaa 100644 --- a/internal/service/otp_mfa_setup.go +++ b/internal/service/otp_mfa_setup.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -142,7 +143,7 @@ func (p *provider) EmailOTPMFASetup(ctx context.Context, meta RequestMetadata, p return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { if err := p.EmailProvider.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]any{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), @@ -150,7 +151,7 @@ func (p *provider) EmailOTPMFASetup(ctx context.Context, meta RequestMetadata, p }); err != nil { log.Debug().Msg("Failed to send otp email") } - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditMFAEnabledEvent, @@ -198,14 +199,14 @@ func (p *provider) SMSOTPMFASetup(ctx context.Context, meta RequestMetadata, par return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { smsBody := strings.Builder{} smsBody.WriteString("Your verification code is: ") smsBody.WriteString(otpData.Otp) if err := p.SMSProvider.SendSMS(phone, smsBody.String()); err != nil { log.Debug().Msg("Failed to send sms") } - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditMFAEnabledEvent, diff --git a/internal/service/resend_otp.go b/internal/service/resend_otp.go index f5c4bf17..b7c0a4f1 100644 --- a/internal/service/resend_otp.go +++ b/internal/service/resend_otp.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -165,7 +166,7 @@ func (p *provider) ResendOTP(ctx context.Context, meta RequestMetadata, params * return nil, nil, err } if email != "" { - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) // exec it as go routine so that we can reduce the api latency if err := p.EmailProvider.SendEmail([]string{email}, constants.VerificationTypeOTP, map[string]any{ @@ -176,9 +177,9 @@ func (p *provider) ResendOTP(ctx context.Context, meta RequestMetadata, params * log.Debug().Err(err).Msg("Failed to send email") } _ = p.EventsProvider.RegisterEvent(ctx, constants.UserLoginWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) - }() + }) } else { - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) smsBody := strings.Builder{} smsBody.WriteString("Your verification code is: ") @@ -187,7 +188,7 @@ func (p *provider) ResendOTP(ctx context.Context, meta RequestMetadata, params * if err := p.SMSProvider.SendSMS(phoneNumber, smsBody.String()); err != nil { log.Debug().Err(err).Msg("Failed to send sms") } - }() + }) } p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditOTPResentEvent, diff --git a/internal/service/resend_verify_email.go b/internal/service/resend_verify_email.go index 2637215f..a7becb5b 100644 --- a/internal/service/resend_verify_email.go +++ b/internal/service/resend_verify_email.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/graph/model" @@ -91,13 +92,13 @@ func (p *provider) ResendVerifyEmail(ctx context.Context, meta RequestMetadata, } // exec it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { _ = p.EmailProvider.SendEmail([]string{params.Email}, params.Identifier, map[string]any{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), "verification_url": utils.GetEmailVerificationURL(verificationToken, hostname, verificationRequest.RedirectURI), }) - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditVerifyEmailResentEvent, Protocol: meta.Protocol, ActorID: user.ID, diff --git a/internal/service/signup.go b/internal/service/signup.go index 62ce7326..03b50fe8 100644 --- a/internal/service/signup.go +++ b/internal/service/signup.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "golang.org/x/crypto/bcrypt" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -233,7 +234,7 @@ func (p *provider) SignUp(ctx context.Context, meta RequestMetadata, params *mod return nil, nil, err } // exec it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.EmailProvider.SendEmail([]string{email}, constants.VerificationTypeBasicAuthSignup, map[string]interface{}{ "user": user.ToMap(), @@ -241,7 +242,7 @@ func (p *provider) SignUp(ctx context.Context, meta RequestMetadata, params *mod "verification_url": utils.GetEmailVerificationURL(verificationToken, hostname, redirectURL), }) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) - }() + }) return &model.AuthResponse{ Message: `Verification email has been sent. Please check your inbox`, @@ -279,11 +280,11 @@ func (p *provider) SignUp(ctx context.Context, meta RequestMetadata, params *mod for _, c := range cookie.BuildMfaSessionCookies(hostname, mfaSession, p.Config.AppCookieSecure, expiresAt) { side.AddCookie(c) } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.SMSProvider.SendSMS(phoneNumber, smsBody.String()) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodMobileBasicAuth, user) - }() + }) return &model.AuthResponse{ Message: "Please check the OTP in your inbox", ShouldShowMobileOtpScreen: refs.NewBoolRef(true), @@ -428,7 +429,7 @@ func (p *provider) SignUp(ctx context.Context, meta RequestMetadata, params *mod ipAddress := meta.IPAddress userAgent := meta.UserAgent - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) _ = p.EventsProvider.RegisterEvent(ctx, constants.UserCreatedWebhookEvent, constants.AuthRecipeMethodBasicAuth, user) if isEmailSignup { @@ -446,7 +447,7 @@ func (p *provider) SignUp(ctx context.Context, meta RequestMetadata, params *mod }); err != nil { log.Debug().Err(err).Msg("Failed to add session") } - }() + }) metrics.RecordAuthEvent(metrics.EventSignup, metrics.StatusSuccess) metrics.ActiveSessions.Inc() p.AuditProvider.LogEvent(audit.Event{ diff --git a/internal/service/update_profile.go b/internal/service/update_profile.go index e0631df6..c7e96c72 100644 --- a/internal/service/update_profile.go +++ b/internal/service/update_profile.go @@ -8,6 +8,7 @@ import ( "golang.org/x/crypto/bcrypt" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -199,7 +200,7 @@ func (p *provider) UpdateProfile(ctx context.Context, meta RequestMetadata, para return nil, nil, InvalidArgument("user with this email address already exists") } - go func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }() + asyncutil.Go(p.Log, func() { _ = p.MemoryStoreProvider.DeleteAllUserSessions(user.ID) }) for _, c := range cookie.BuildDeleteSessionCookies(meta.HostURL, p.Config.AppCookieSecure, cookie.ParseSameSite(p.Config.AppCookieSameSite)) { side.AddCookie(c) } @@ -243,13 +244,13 @@ func (p *provider) UpdateProfile(ctx context.Context, meta RequestMetadata, para } // exec it as go routine so that we can reduce the api latency - go func() { + asyncutil.Go(p.Log, func() { _ = p.EmailProvider.SendEmail([]string{refs.StringValue(user.Email)}, verificationType, map[string]any{ "user": user.ToMap(), "organization": utils.GetOrganization(p.Config), "verification_url": utils.GetEmailVerificationURL(verificationToken, hostname, redirectURL), }) - }() + }) } } diff --git a/internal/service/verify_email.go b/internal/service/verify_email.go index d471284d..d6cd9436 100644 --- a/internal/service/verify_email.go +++ b/internal/service/verify_email.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/authorizerdev/authorizer/internal/asyncutil" "github.com/authorizerdev/authorizer/internal/audit" "github.com/authorizerdev/authorizer/internal/constants" "github.com/authorizerdev/authorizer/internal/cookie" @@ -99,7 +100,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params log.Debug().Err(err).Msg("Failed to set mfa session") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) if err := p.EmailProvider.SendEmail([]string{refs.StringValue(user.Email)}, constants.VerificationTypeOTP, map[string]any{ "user": user.ToMap(), @@ -109,7 +110,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params log.Debug().Err(err).Msg("Failed to send otp email") } _ = p.EventsProvider.RegisterEvent(ctx, constants.UserLoginWebhookEvent, loginMethod, user) - }() + }) return &model.AuthResponse{ Message: "Please check email inbox for the OTP", ShouldShowEmailOtpScreen: refs.NewBoolRef(true), @@ -129,7 +130,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params log.Debug().Err(err).Msg("Failed to set mfa session") return nil, nil, err } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) smsBody := strings.Builder{} smsBody.WriteString("Your verification code is: ") @@ -138,7 +139,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params if err := p.SMSProvider.SendSMS(refs.StringValue(user.PhoneNumber), smsBody.String()); err != nil { log.Debug().Err(err).Msg("Failed to send sms") } - }() + }) return &model.AuthResponse{ Message: "Please check text message for the OTP", ShouldShowMobileOtpScreen: refs.NewBoolRef(true), @@ -250,7 +251,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params } else { nonce = authorizeState } - go func() { _ = p.MemoryStoreProvider.RemoveState(refs.StringValue(params.State)) }() + asyncutil.Go(p.Log, func() { _ = p.MemoryStoreProvider.RemoveState(refs.StringValue(params.State)) }) } } if nonce == "" { @@ -282,7 +283,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params // return nil, err // } // } - go func() { + asyncutil.Go(p.Log, func() { ctx := context.WithoutCancel(ctx) if isSignUp { _ = p.EventsProvider.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user) @@ -299,7 +300,7 @@ func (p *provider) VerifyEmail(ctx context.Context, meta RequestMetadata, params }); err != nil { log.Debug().Err(err).Msg("Failed to add session") } - }() + }) p.AuditProvider.LogEvent(audit.Event{ Action: constants.AuditEmailVerifiedEvent, Protocol: meta.Protocol, ActorID: user.ID,