This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The CSRF wall (spec §6.4): a state-changing call made with the session cookie carries the
|
||||
// page's token, and a bearer or API-key caller is not asked for one, because no browser attaches
|
||||
// those by itself.
|
||||
|
||||
import type { Principal } from "@conusai-kit/auth"
|
||||
import { AppError } from "@conusai-kit/core"
|
||||
import { beforeAll, describe, expect, it } from "vitest"
|
||||
|
||||
const session = (sessionId: string): Principal => ({
|
||||
actor: { userId: "11111111-1111-1111-1111-111111111111" },
|
||||
method: "session",
|
||||
sessionId,
|
||||
})
|
||||
|
||||
let csrfToken: (sessionId: string) => string
|
||||
let requireCsrf: (request: Request, principal: Principal | null) => void
|
||||
|
||||
beforeAll(async () => {
|
||||
// loadAuthConfig reads the environment at call time, so the secret is set before the import
|
||||
// that uses it.
|
||||
process.env.AUTH_SESSION_SECRET = "0123456789abcdef0123456789abcdef"
|
||||
process.env.CONUSAI_PUBLIC_URL = "https://app.example.test"
|
||||
process.env.OIDC_ISSUER = "https://id.example.test"
|
||||
process.env.OIDC_WEB_CLIENT_ID = "web"
|
||||
process.env.OIDC_AUDIENCE = "api"
|
||||
const module = await import("../apps/web/src/server/csrf")
|
||||
csrfToken = module.csrfToken
|
||||
requireCsrf = module.requireCsrf
|
||||
})
|
||||
|
||||
const post = (headers: Record<string, string>): Request =>
|
||||
new Request("https://app.example.test/api/v1/tenancy.org.create", { method: "POST", headers })
|
||||
|
||||
describe("requireCsrf", () => {
|
||||
it("accepts a POST carrying this session's token", () => {
|
||||
const principal = session("session-1")
|
||||
expect(() =>
|
||||
requireCsrf(post({ "x-csrf-token": csrfToken("session-1"), "sec-fetch-site": "same-origin" }), principal),
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it("refuses a POST with no token", () => {
|
||||
expect(() => requireCsrf(post({}), session("session-1"))).toThrow(AppError)
|
||||
})
|
||||
|
||||
it("refuses another session's token", () => {
|
||||
expect(() => requireCsrf(post({ "x-csrf-token": csrfToken("session-2") }), session("session-1"))).toThrow(
|
||||
/x-csrf-token/,
|
||||
)
|
||||
})
|
||||
|
||||
it("refuses a cross-site POST even with a token", () => {
|
||||
const headers = { "x-csrf-token": csrfToken("session-1"), "sec-fetch-site": "cross-site" }
|
||||
expect(() => requireCsrf(post(headers), session("session-1"))).toThrow(/cross-site/)
|
||||
})
|
||||
|
||||
it("asks nothing of a bearer or API-key caller", () => {
|
||||
const bearer: Principal = { actor: { userId: "u" }, method: "bearer" }
|
||||
const key: Principal = { actor: { userId: "u" }, method: "api-key", orgId: "o" }
|
||||
expect(() => requireCsrf(post({}), bearer)).not.toThrow()
|
||||
expect(() => requireCsrf(post({}), key)).not.toThrow()
|
||||
})
|
||||
|
||||
it("asks every browser session method for its token, including dev-login and an external provider session", () => {
|
||||
const dev: Principal = { actor: { userId: "u" }, method: "dev-login", sessionId: "session-1" }
|
||||
const external: Principal = {
|
||||
actor: { userId: "u" },
|
||||
method: "external-session",
|
||||
sessionRef: { provider: "convex-better-auth", opaqueId: "session-1" },
|
||||
}
|
||||
expect(() => requireCsrf(post({}), dev)).toThrow(/x-csrf-token/)
|
||||
expect(() => requireCsrf(post({}), external)).toThrow(/x-csrf-token/)
|
||||
// An external session's token is bound to its provider reference, never to a kit row of the same ID.
|
||||
expect(() => requireCsrf(post({ "x-csrf-token": csrfToken("session-1") }), external)).toThrow(/x-csrf-token/)
|
||||
})
|
||||
|
||||
it("fails closed on an external session disguised with a kit session row", () => {
|
||||
const disguised: Principal = {
|
||||
actor: { userId: "u" },
|
||||
method: "external-session",
|
||||
sessionId: "session-1",
|
||||
sessionRef: { provider: "convex-better-auth", opaqueId: "session-1" },
|
||||
}
|
||||
expect(() => requireCsrf(post({ "x-csrf-token": csrfToken("session-1") }), disguised)).toThrow(AppError)
|
||||
})
|
||||
|
||||
it("asks nothing of a GET", () => {
|
||||
const request = new Request("https://app.example.test/api/health", { headers: {} })
|
||||
expect(() => requireCsrf(request, session("session-1"))).not.toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user