diff --git a/server/internal/auth/stepup.go b/server/internal/auth/stepup.go index e57a8ff..9fd6a31 100644 --- a/server/internal/auth/stepup.go +++ b/server/internal/auth/stepup.go @@ -41,6 +41,10 @@ func RequireStepUp() gin.HandlerFunc { c.Next() return } + if sess == nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"}) + return + } methods, err := services.MFAMethods(sess.InstanceID, sess.UserID) if err != nil || len(methods) == 0 { methods = []string{services.FactorPassword} diff --git a/server/internal/auth/stepup_test.go b/server/internal/auth/stepup_test.go index aa60d9d..bb29ec3 100644 --- a/server/internal/auth/stepup_test.go +++ b/server/internal/auth/stepup_test.go @@ -1,8 +1,12 @@ package auth import ( + "net/http" + "net/http/httptest" "testing" "time" + + "github.com/gin-gonic/gin" ) func TestStepUpFresh(t *testing.T) { @@ -32,3 +36,19 @@ func TestStepUpFresh(t *testing.T) { }) } } + +// A missing session must refuse rather than panic dereferencing sess. +// auth.Middleware guarantees a non-nil session on every route today, but +// RequireStepUp must not rely on that holding forever. +func TestRequireStepUpNilSessionRefusesWithoutPanic(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/console/connect", nil) + + RequireStepUp()(c) + + if w.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want %d", w.Code, http.StatusUnauthorized) + } +}