Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/api/src/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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"
Expand Down
60 changes: 60 additions & 0 deletions apps/api/src/tests/oauthState.test.js
Original file line number Diff line number Diff line change
@@ -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"
}
});
});
});
Loading