Skip to content

Commit d2dd774

Browse files
fix(gateway): skip gateway auth for LINE webhook routes with noGatewayAuth
The shouldEnforceGatewayAuthForPluginPath function was enforcing gateway- level auth on all registered plugin HTTP routes, including the LINE webhook which implements its own signature-based auth. This caused POST /line/webhook to receive 401/405 responses after the auth gate was introduced. Add a noGatewayAuth flag to PluginHttpRouteRegistration so webhook routes can opt out of gateway auth enforcement. LINE monitor now sets this flag when registering its webhook route. Fixes #31599 Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 4dcb16d commit d2dd774

File tree

6 files changed

+64
-3
lines changed

6 files changed

+64
-3
lines changed

src/gateway/server.plugin-http-auth.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,40 @@ describe("gateway plugin HTTP auth boundary", () => {
477477
});
478478
});
479479

480+
test("allows POST to webhook routes with noGatewayAuth without auth token", async () => {
481+
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
482+
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
483+
if (pathname === "/line/webhook") {
484+
res.statusCode = 200;
485+
res.setHeader("Content-Type", "application/json; charset=utf-8");
486+
res.end(JSON.stringify({ ok: true, route: "line-webhook" }));
487+
return true;
488+
}
489+
return false;
490+
});
491+
492+
await withGatewayServer({
493+
prefix: "openclaw-plugin-http-auth-webhook-noauth-test-",
494+
resolvedAuth: AUTH_TOKEN,
495+
overrides: {
496+
handlePluginRequest,
497+
// Simulate a predicate that exempts noGatewayAuth routes (like the real one)
498+
shouldEnforcePluginGatewayAuth: (requestPath) =>
499+
requestPath.startsWith("/api/channels") || requestPath === "/managed/route",
500+
},
501+
run: async (server) => {
502+
// POST to /line/webhook without auth should reach the plugin handler
503+
const postResponse = await sendRequest(server, {
504+
path: "/line/webhook",
505+
method: "POST",
506+
});
507+
expect(postResponse.res.statusCode).toBe(200);
508+
expect(postResponse.getBody()).toContain('"route":"line-webhook"');
509+
expect(handlePluginRequest).toHaveBeenCalledTimes(1);
510+
},
511+
});
512+
});
513+
480514
test("uses /api/channels auth by default while keeping wildcard handlers ungated with no predicate", async () => {
481515
const handlePluginRequest = vi.fn(async (req: IncomingMessage, res: ServerResponse) => {
482516
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;

src/gateway/server/plugins-http.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,17 @@ describe("plugin HTTP registry helpers", () => {
152152
expect(shouldEnforceGatewayAuthForPluginPath(registry, "/api/channels/status")).toBe(true);
153153
expect(shouldEnforceGatewayAuthForPluginPath(registry, "/not-plugin")).toBe(false);
154154
});
155+
156+
it("skips auth enforcement for routes with noGatewayAuth", () => {
157+
const registry = createTestRegistry({
158+
httpRoutes: [
159+
{ ...createRoute({ path: "/line/webhook" }), noGatewayAuth: true },
160+
createRoute({ path: "/api/managed" }),
161+
],
162+
});
163+
expect(shouldEnforceGatewayAuthForPluginPath(registry, "/line/webhook")).toBe(false);
164+
expect(shouldEnforceGatewayAuthForPluginPath(registry, "/api/managed")).toBe(true);
165+
// Protected prefix routes are always gated regardless of noGatewayAuth
166+
expect(shouldEnforceGatewayAuthForPluginPath(registry, "/api/channels/status")).toBe(true);
167+
});
155168
});

src/gateway/server/plugins-http.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ export function shouldEnforceGatewayAuthForPluginPath(
3636
registry: PluginRegistry,
3737
pathname: string,
3838
): boolean {
39-
return (
40-
isProtectedPluginRoutePath(pathname) || isRegisteredPluginHttpRoutePath(registry, pathname)
41-
);
39+
if (isProtectedPluginRoutePath(pathname)) {
40+
return true;
41+
}
42+
// Only enforce gateway auth on registered plugin routes that have NOT
43+
// opted out via noGatewayAuth. Channel webhook routes (e.g. LINE)
44+
// implement their own signature-based auth and must remain reachable
45+
// without a gateway token.
46+
const route = findRegisteredPluginHttpRoute(registry, pathname);
47+
return route !== undefined && !route.noGatewayAuth;
4248
}
4349

4450
export function createGatewayPluginRequestHandler(params: {

src/line/monitor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export async function monitorLineProvider(
292292
accountId: resolvedAccountId,
293293
log: (msg) => logVerbose(msg),
294294
handler: createLineNodeWebhookHandler({ channelSecret: secret, bot, runtime }),
295+
noGatewayAuth: true,
295296
});
296297

297298
logVerbose(`line: registered webhook handler at ${normalizedPath}`);

src/plugins/http-registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function registerPluginHttpRoute(params: {
1717
accountId?: string;
1818
log?: (message: string) => void;
1919
registry?: PluginRegistry;
20+
/** When true, skip gateway-level auth enforcement for this route. */
21+
noGatewayAuth?: boolean;
2022
}): () => void {
2123
const registry = params.registry ?? requireActivePluginRegistry();
2224
const routes = registry.httpRoutes ?? [];
@@ -41,6 +43,7 @@ export function registerPluginHttpRoute(params: {
4143
handler: params.handler,
4244
pluginId: params.pluginId,
4345
source: params.source,
46+
noGatewayAuth: params.noGatewayAuth,
4447
};
4548
routes.push(entry);
4649

src/plugins/registry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export type PluginHttpRouteRegistration = {
6060
path: string;
6161
handler: OpenClawPluginHttpRouteHandler;
6262
source?: string;
63+
/** When true, skip gateway-level auth enforcement for this route.
64+
* Webhook routes that implement their own signature-based auth
65+
* (e.g., LINE webhook) should set this to true. */
66+
noGatewayAuth?: boolean;
6367
};
6468

6569
export type PluginChannelRegistration = {

0 commit comments

Comments
 (0)