diff --git a/apps/api/src/controllers/authController.js b/apps/api/src/controllers/authController.js index 000855c09d..4db08387a5 100644 --- a/apps/api/src/controllers/authController.js +++ b/apps/api/src/controllers/authController.js @@ -1,6 +1,6 @@ import { registerSchema, loginSchema } from "../validators/auth.js"; import { loginUser, refreshToken, registerUser } from "../services/authService.js"; -import { ok } from "../utils/response.js"; +import { fail, ok } from "../utils/response.js"; export async function register(req, res) { const payload = registerSchema.parse(req.body); @@ -15,6 +15,11 @@ export async function login(req, res) { } export async function oauthCallback(req, res) { + const { state } = req.query; + if (typeof state !== "string" || state.trim().length === 0) { + return fail(res, "OAuth state is required", 400); + } + return ok(res, { provider: req.params.provider, status: "callback-received" diff --git a/apps/api/src/tests/oauthState.test.js b/apps/api/src/tests/oauthState.test.js new file mode 100644 index 0000000000..00fbd06615 --- /dev/null +++ b/apps/api/src/tests/oauthState.test.js @@ -0,0 +1,60 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createApp } from "../app.js"; + +async function withServer(run) { + const app = createApp(); + const server = app.listen(0); + + await new Promise((resolve, reject) => { + server.once("listening", resolve); + server.once("error", reject); + }); + + try { + const { port } = server.address(); + return await run(`http://127.0.0.1:${port}`); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +} + +test("OAuth callback rejects missing state", async () => { + await withServer(async (baseUrl) => { + const response = await fetch(`${baseUrl}/api/auth/oauth/github/callback?code=abc123`); + const payload = await response.json(); + + assert.equal(response.status, 400); + assert.deepEqual(payload, { success: false, message: "OAuth state is required" }); + }); +}); + +test("OAuth callback rejects repeated state values", async () => { + await withServer(async (baseUrl) => { + const response = await fetch( + `${baseUrl}/api/auth/oauth/github/callback?code=abc123&state=first&state=second` + ); + const payload = await response.json(); + + assert.equal(response.status, 400); + assert.deepEqual(payload, { success: false, message: "OAuth state is required" }); + }); +}); + +test("OAuth callback accepts a single non-empty state", async () => { + await withServer(async (baseUrl) => { + const response = await fetch(`${baseUrl}/api/auth/oauth/github/callback?code=abc123&state=csrf-token`); + const payload = await response.json(); + + assert.equal(response.status, 200); + assert.deepEqual(payload, { + success: true, + data: { + provider: "github", + status: "callback-received" + } + }); + }); +});