Skip to content

Commit 5d52769

Browse files
committed
Validate OAuth callback state
1 parent 9558677 commit 5d52769

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

apps/api/src/controllers/authController.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { registerSchema, loginSchema } from "../validators/auth.js";
22
import { loginUser, refreshToken, registerUser } from "../services/authService.js";
3-
import { ok } from "../utils/response.js";
3+
import { fail, ok } from "../utils/response.js";
44

55
export async function register(req, res) {
66
const payload = registerSchema.parse(req.body);
@@ -15,6 +15,11 @@ export async function login(req, res) {
1515
}
1616

1717
export async function oauthCallback(req, res) {
18+
const { state } = req.query;
19+
if (typeof state !== "string" || state.trim().length === 0) {
20+
return fail(res, "OAuth state is required", 400);
21+
}
22+
1823
return ok(res, {
1924
provider: req.params.provider,
2025
status: "callback-received"
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { createApp } from "../app.js";
4+
5+
async function withServer(run) {
6+
const app = createApp();
7+
const server = app.listen(0);
8+
9+
await new Promise((resolve, reject) => {
10+
server.once("listening", resolve);
11+
server.once("error", reject);
12+
});
13+
14+
try {
15+
const { port } = server.address();
16+
return await run(`http://127.0.0.1:${port}`);
17+
} finally {
18+
await new Promise((resolve, reject) => {
19+
server.close((error) => (error ? reject(error) : resolve()));
20+
});
21+
}
22+
}
23+
24+
test("OAuth callback rejects missing state", async () => {
25+
await withServer(async (baseUrl) => {
26+
const response = await fetch(`${baseUrl}/api/auth/oauth/github/callback?code=abc123`);
27+
const payload = await response.json();
28+
29+
assert.equal(response.status, 400);
30+
assert.deepEqual(payload, { success: false, message: "OAuth state is required" });
31+
});
32+
});
33+
34+
test("OAuth callback rejects repeated state values", async () => {
35+
await withServer(async (baseUrl) => {
36+
const response = await fetch(
37+
`${baseUrl}/api/auth/oauth/github/callback?code=abc123&state=first&state=second`
38+
);
39+
const payload = await response.json();
40+
41+
assert.equal(response.status, 400);
42+
assert.deepEqual(payload, { success: false, message: "OAuth state is required" });
43+
});
44+
});
45+
46+
test("OAuth callback accepts a single non-empty state", async () => {
47+
await withServer(async (baseUrl) => {
48+
const response = await fetch(`${baseUrl}/api/auth/oauth/github/callback?code=abc123&state=csrf-token`);
49+
const payload = await response.json();
50+
51+
assert.equal(response.status, 200);
52+
assert.deepEqual(payload, {
53+
success: true,
54+
data: {
55+
provider: "github",
56+
status: "callback-received"
57+
}
58+
});
59+
});
60+
});

0 commit comments

Comments
 (0)