package logger import ( "context" "fmt" "log/slog" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) // AuditLogEntry holds common audit logging fields. type AuditLogEntry struct { RequestID string Stage string Operation string // e.g., "SendPasswordReset", "Verify" Method string Path string Status int LatencyMs time.Duration IP string UserAgent string Origin string Referer string Query map[string]string Headers map[string]string // 핵심 헤더(민감 키는 마스킹됨) LoginIDs map[string]string // loginId and loginId_normalized Token string // For reset tokens, magic link tokens ProviderError string ProviderStatus int // Provider HTTP status ProviderBody string // Provider response body (full raw) RefreshToken string SessionJwt string AccessJwt string UserLoginId string UserID string Email string Phone string SetCookieName string SetCookieValue string SetCookieAttrs map[string]string RedirectTo string HasCookieDSRF bool ParsedCookieDSRF string // ... potentially more fields specific to different stages } // NewAuditLogEntry creates a new AuditLogEntry with a generated RequestID and initial common fields. func NewAuditLogEntry(c *fiber.Ctx, stage string) *AuditLogEntry { reqID := uuid.New().String() // Extract query parameters queryParams := make(map[string]string) c.Context().QueryArgs().VisitAll(func(key, value []byte) { k := string(key) queryParams[k] = maskSensitiveByKey(k, string(value)) }) // Extract relevant headers headers := make(map[string]string) headers["Host"] = c.Get("Host") headers["User-Agent"] = c.Get("User-Agent") headers["Origin"] = c.Get("Origin") headers["Referer"] = c.Get("Referer") return &AuditLogEntry{ RequestID: reqID, Stage: stage, Method: c.Method(), Path: c.Path(), IP: c.IP(), UserAgent: c.Get("User-Agent"), Origin: c.Get("Origin"), Referer: c.Get("Referer"), Query: queryParams, Headers: headers, LoginIDs: make(map[string]string), SetCookieAttrs: make(map[string]string), } } // Log emits an audit log entry using slog. // It includes common fields and allows for additional custom fields. func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) { attrs := []slog.Attr{ slog.String("req_id", ale.RequestID), slog.String("stage", ale.Stage), } if ale.Operation != "" { attrs = append(attrs, slog.String("op", ale.Operation)) } if ale.Method != "" { attrs = append(attrs, slog.String("method", ale.Method)) } if ale.Path != "" { attrs = append(attrs, slog.String("path", ale.Path)) } if ale.Status != 0 { attrs = append(attrs, slog.Int("status", ale.Status)) } if ale.LatencyMs != 0 { attrs = append(attrs, slog.Duration("latency_ms", ale.LatencyMs)) } if ale.IP != "" { attrs = append(attrs, slog.String("ip", ale.IP)) } if ale.UserAgent != "" { attrs = append(attrs, slog.String("user_agent", ale.UserAgent)) } if ale.Origin != "" { attrs = append(attrs, slog.String("origin", ale.Origin)) } if ale.Referer != "" { attrs = append(attrs, slog.String("referer", ale.Referer)) } if len(ale.Query) > 0 { queryGroupArgs := make([]any, 0, len(ale.Query)) for k, v := range ale.Query { queryGroupArgs = append(queryGroupArgs, slog.String(k, maskSensitiveByKey(k, v))) } attrs = append(attrs, slog.Group("query", queryGroupArgs...)) } if len(ale.Headers) > 0 { headersGroupArgs := make([]any, 0, len(ale.Headers)) for k, v := range ale.Headers { headersGroupArgs = append(headersGroupArgs, slog.String(k, maskSensitiveByKey(k, v))) } attrs = append(attrs, slog.Group("headers", headersGroupArgs...)) } if len(ale.LoginIDs) > 0 { loginIDGroupArgs := make([]any, 0, len(ale.LoginIDs)) for k, v := range ale.LoginIDs { loginIDGroupArgs = append(loginIDGroupArgs, slog.String(k, v)) } attrs = append(attrs, slog.Group("login_ids", loginIDGroupArgs...)) } if ale.Token != "" { attrs = append(attrs, slog.Bool("has_token", true)) } if ale.ProviderError != "" { attrs = append(attrs, slog.String("provider_error", ale.ProviderError)) } if ale.ProviderStatus != 0 { attrs = append(attrs, slog.Int("provider_http_status", ale.ProviderStatus)) } if ale.ProviderBody != "" { attrs = append(attrs, slog.String("provider_response_body", ale.ProviderBody)) } if ale.RefreshToken != "" { attrs = append(attrs, slog.Bool("has_refresh_token", true)) } if ale.SessionJwt != "" { attrs = append(attrs, slog.Bool("has_session_jwt", true)) } if ale.AccessJwt != "" { attrs = append(attrs, slog.Bool("has_access_jwt", true)) } if ale.UserLoginId != "" { attrs = append(attrs, slog.String("user_login_id", ale.UserLoginId)) } if ale.UserID != "" { attrs = append(attrs, slog.String("user_id", ale.UserID)) } if ale.Email != "" { attrs = append(attrs, slog.String("email", ale.Email)) } if ale.Phone != "" { attrs = append(attrs, slog.String("phone", ale.Phone)) } if ale.SetCookieName != "" { attrs = append(attrs, slog.String("set_cookie_name", ale.SetCookieName)) if ale.SetCookieValue != "" { attrs = append(attrs, slog.Bool("has_set_cookie_value", true)) } if len(ale.SetCookieAttrs) > 0 { cookieAttrsGroupArgs := make([]any, 0, len(ale.SetCookieAttrs)) for k, v := range ale.SetCookieAttrs { cookieAttrsGroupArgs = append(cookieAttrsGroupArgs, slog.String(k, v)) } attrs = append(attrs, slog.Group("set_cookie_attrs", cookieAttrsGroupArgs...)) } } if ale.RedirectTo != "" { attrs = append(attrs, slog.String("redirect_to", ale.RedirectTo)) } if ale.HasCookieDSRF { attrs = append(attrs, slog.Bool("has_cookie_DSRF", ale.HasCookieDSRF)) } if ale.ParsedCookieDSRF != "" { attrs = append(attrs, slog.Bool("has_parsed_cookie_DSRF", true)) } // Convert variadic args to slog.Attr before appending for i := 0; i < len(args); i += 2 { if i+1 < len(args) { attrs = append(attrs, slog.Any(fmt.Sprintf("%v", args[i]), args[i+1])) } else { // Handle odd number of arguments - log the last one with a generic key attrs = append(attrs, slog.Any(fmt.Sprintf("extra_arg_%d", i), args[i])) } } slog.Default().LogAttrs(context.Background(), level, msg, attrs...) } var sensitiveAuditKeys = map[string]struct{}{ "password": {}, "currentpassword": {}, "newpassword": {}, "oldpassword": {}, "token": {}, "accesstoken": {}, "refreshtoken": {}, "authorization": {}, "cookie": {}, "setcookie": {}, "verificationcode": {}, "code": {}, "loginchallenge": {}, "loginverifier": {}, "sessionjwt": {}, "accessjwt": {}, "refreshjwt": {}, } func maskSensitiveByKey(key, value string) string { if value == "" { return value } k := strings.ToLower(key) k = strings.ReplaceAll(k, "-", "") k = strings.ReplaceAll(k, "_", "") if _, ok := sensitiveAuditKeys[k]; ok { return "*****" } return value }