diff --git a/src/widgets/crowdsec/proxy.js b/src/widgets/crowdsec/proxy.js index d3257fa6f..9d41e6b2a 100644 --- a/src/widgets/crowdsec/proxy.js +++ b/src/widgets/crowdsec/proxy.js @@ -25,13 +25,25 @@ async function login(widget, service) { }), }); - const dataParsed = JSON.parse(data); + let dataParsed; + try { + dataParsed = JSON.parse(data); + } catch { + logger.error("Failed to parse Crowdsec login response, status: %d", status); + cache.del(`${sessionTokenCacheKey}.${service}`); + return null; + } - if (!(status === 200) || !dataParsed.token) { + if (status !== 200 || !dataParsed.token) { logger.error("Failed to login to Crowdsec API, status: %d", status); cache.del(`${sessionTokenCacheKey}.${service}`); + return null; } - cache.put(`${sessionTokenCacheKey}.${service}`, dataParsed.token, new Date(dataParsed.expire) - new Date()); + + const ttl = Math.max(new Date(dataParsed.expire) - new Date(), 1); + cache.put(`${sessionTokenCacheKey}.${service}`, dataParsed.token, ttl); + + return dataParsed.token; } export default async function crowdsecProxyHandler(req, res) { @@ -48,11 +60,10 @@ export default async function crowdsecProxyHandler(req, res) { return res.status(400).json({ error: "Invalid widget configuration" }); } - if (!cache.get(`${sessionTokenCacheKey}.${service}`)) { - await login(widget, service); + let token = cache.get(`${sessionTokenCacheKey}.${service}`); + if (!token) { + token = await login(widget, service); } - - const token = cache.get(`${sessionTokenCacheKey}.${service}`); if (!token) { return res.status(500).json({ error: "Failed to authenticate with Crowdsec" }); } @@ -71,7 +82,20 @@ export default async function crowdsecProxyHandler(req, res) { logger.debug("Calling Crowdsec API endpoint: %s", endpoint); - const [status, , data] = await httpProxy(url, params); + let [status, , data] = await httpProxy(url, params); + + if (status === 401) { + logger.debug("Crowdsec API returned 401, refreshing token and retrying request"); + cache.del(`${sessionTokenCacheKey}.${service}`); + const refreshedToken = await login(widget, service); + + if (!refreshedToken) { + return res.status(500).json({ error: "Failed to authenticate with Crowdsec" }); + } + + params.headers.Authorization = `Bearer ${refreshedToken}`; + [status, , data] = await httpProxy(url, params); + } if (status !== 200) { logger.error("Error calling Crowdsec API: %d. Data: %s", status, data); diff --git a/src/widgets/crowdsec/proxy.test.js b/src/widgets/crowdsec/proxy.test.js index 7555cf16a..157a96a53 100644 --- a/src/widgets/crowdsec/proxy.test.js +++ b/src/widgets/crowdsec/proxy.test.js @@ -89,4 +89,76 @@ describe("widgets/crowdsec/proxy", () => { expect(res.statusCode).toBe(500); expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" }); }); + + it("re-authenticates and retries once when API returns 401", async () => { + getServiceWidget.mockResolvedValue({ + type: "crowdsec", + url: "http://cs", + username: "machine", + password: "pw", + }); + + httpProxy + .mockResolvedValueOnce([ + 200, + "application/json", + JSON.stringify({ token: "tok-old", expire: new Date(Date.now() + 60_000).toISOString() }), + ]) + .mockResolvedValueOnce([401, "application/json", Buffer.from("bad token")]) + .mockResolvedValueOnce([ + 200, + "application/json", + JSON.stringify({ token: "tok-new", expire: new Date(Date.now() + 60_000).toISOString() }), + ]) + .mockResolvedValueOnce([200, "application/json", Buffer.from("data")]); + + const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } }; + const res = createMockRes(); + + await crowdsecProxyHandler(req, res); + + expect(httpProxy).toHaveBeenCalledTimes(4); + expect(httpProxy.mock.calls[3][1].headers.Authorization).toBe("Bearer tok-new"); + expect(res.statusCode).toBe(200); + expect(res.body).toEqual(Buffer.from("data")); + }); + + it("returns 500 when 401 refresh fails to get a new token", async () => { + getServiceWidget.mockResolvedValue({ + type: "crowdsec", + url: "http://cs", + username: "machine", + password: "pw", + }); + + httpProxy + .mockResolvedValueOnce([ + 200, + "application/json", + JSON.stringify({ token: "tok-old", expire: new Date(Date.now() + 60_000).toISOString() }), + ]) + .mockResolvedValueOnce([401, "application/json", Buffer.from("bad token")]) + .mockResolvedValueOnce([500, "application/json", JSON.stringify({ error: "no token" })]); + + const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } }; + const res = createMockRes(); + + await crowdsecProxyHandler(req, res); + + expect(res.statusCode).toBe(500); + expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" }); + }); + + it("returns 500 when login response is not JSON", async () => { + getServiceWidget.mockResolvedValue({ type: "crowdsec", url: "http://cs", username: "machine", password: "pw" }); + httpProxy.mockResolvedValueOnce([200, "text/plain", "not-json"]); + + const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } }; + const res = createMockRes(); + + await crowdsecProxyHandler(req, res); + + expect(res.statusCode).toBe(500); + expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" }); + }); });