diff --git a/pkg/services/frontend/frontend_service_test.go b/pkg/services/frontend/frontend_service_test.go index ff60cb73c03..434f1b37e4a 100644 --- a/pkg/services/frontend/frontend_service_test.go +++ b/pkg/services/frontend/frontend_service_test.go @@ -187,6 +187,112 @@ func TestFrontendService_Middleware(t *testing.T) { }) } +func TestFrontendService_LoginErrorCookie(t *testing.T) { + publicDir := setupTestWebAssets(t) + cfg := &setting.Cfg{ + HTTPPort: "3000", + StaticRootPath: publicDir, + BuildVersion: "10.3.0", + OAuthLoginErrorMessage: "oauth.login.error", + CookieSecure: false, + CookieSameSiteDisabled: false, + CookieSameSiteMode: http.SameSiteLaxMode, + } + + t.Run("should detect login_error cookie and set generic error message", func(t *testing.T) { + service := createTestService(t, cfg) + + mux := web.New() + service.addMiddlewares(mux) + service.registerRoutes(mux) + + req := httptest.NewRequest("GET", "/", nil) + // Set the login_error cookie (with some encrypted-looking value) + req.AddCookie(&http.Cookie{ + Name: "login_error", + Value: "abc123encryptedvalue", + }) + recorder := httptest.NewRecorder() + + mux.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + body := recorder.Body.String() + + // Check that the generic error message is in the response + assert.Contains(t, body, "loginError", "Should contain loginError when cookie is present") + assert.Contains(t, body, "oauth.login.error", "Should contain the generic OAuth error message") + + // Check that the cookie was deleted (MaxAge=-1) + cookies := recorder.Result().Cookies() + var foundDeletedCookie bool + for _, cookie := range cookies { + if cookie.Name == "login_error" { + assert.Equal(t, -1, cookie.MaxAge, "Cookie should be deleted (MaxAge=-1)") + assert.Equal(t, "", cookie.Value, "Cookie value should be empty") + foundDeletedCookie = true + break + } + } + assert.True(t, foundDeletedCookie, "Should have set a cookie deletion header") + }) + + t.Run("should not set error when login_error cookie is absent", func(t *testing.T) { + service := createTestService(t, cfg) + + mux := web.New() + service.addMiddlewares(mux) + service.registerRoutes(mux) + + req := httptest.NewRequest("GET", "/", nil) + // No login_error cookie + recorder := httptest.NewRecorder() + + mux.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + body := recorder.Body.String() + + // The page should render but without the login error + assert.Contains(t, body, "window.grafanaBootData") + // Check that loginError is not set (or is empty/omitted in JSON) + // Since it's omitempty, it shouldn't appear at all + assert.NotContains(t, body, "loginError", "Should not contain loginError when cookie is absent") + }) + + t.Run("should handle custom OAuth error message from config", func(t *testing.T) { + customCfg := &setting.Cfg{ + HTTPPort: "3000", + StaticRootPath: publicDir, + BuildVersion: "10.3.0", + OAuthLoginErrorMessage: "Oh no a boo-boo happened!", + CookieSecure: false, + CookieSameSiteDisabled: false, + CookieSameSiteMode: http.SameSiteLaxMode, + } + service := createTestService(t, customCfg) + + mux := web.New() + service.addMiddlewares(mux) + service.registerRoutes(mux) + + req := httptest.NewRequest("GET", "/", nil) + req.AddCookie(&http.Cookie{ + Name: "login_error", + Value: "abc123encryptedvalue", + }) + recorder := httptest.NewRecorder() + + mux.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + body := recorder.Body.String() + + // Check that the custom error message is used + assert.Contains(t, body, "Oh no a boo-boo happened!", "Should use custom OAuth error message from config") + }) +} + func TestFrontendService_IndexHooks(t *testing.T) { publicDir := setupTestWebAssets(t) cfg := &setting.Cfg{ diff --git a/pkg/services/frontend/frontend_settings.go b/pkg/services/frontend/frontend_settings.go index d9476c8ff40..65fcb8ab192 100644 --- a/pkg/services/frontend/frontend_settings.go +++ b/pkg/services/frontend/frontend_settings.go @@ -47,4 +47,6 @@ type FSFrontendSettings struct { CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled,omitempty"` Http2Enabled bool `json:"http2Enabled,omitempty"` ReportingStaticContext map[string]string `json:"reportingStaticContext,omitempty"` + + LoginError string `json:"loginError,omitempty"` } diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index 25704f1ab6d..cefc422d1c0 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -148,6 +148,30 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. data.Nonce = nonce data.PublicDashboardAccessToken = reqCtx.PublicDashboardAccessToken + // TODO -- reevaluate with mt authnz + // Check for login_error cookie and set a generic error message. + // The backend sets an encrypted cookie on oauth login failures that we can't read + // so we just show a generic error if the cookie is present. + if cookie, err := request.Cookie("login_error"); err == nil && cookie.Value != "" { + p.log.Info("request has login_error cookie") + // Defaults to a translation key that the frontend will resolve to a localized message + data.Settings.LoginError = p.data.Config.OAuthLoginErrorMessage + + cookiePath := "/" + if p.data.AppSubUrl != "" { + cookiePath = p.data.AppSubUrl + } + http.SetCookie(writer, &http.Cookie{ + Name: "login_error", + Value: "", + Path: cookiePath, + MaxAge: -1, + HttpOnly: true, + Secure: p.data.Config.CookieSecure, + SameSite: p.data.Config.CookieSameSiteMode, + }) + } + if data.CSPEnabled { data.CSPContent = middleware.ReplacePolicyVariables(p.data.CSPContent, p.data.AppSubUrl, data.Nonce) writer.Header().Set("Content-Security-Policy", data.CSPContent)