diff --git a/src/db/sqlite/SqliteInterface.ts b/src/db/sqlite/SqliteInterface.ts index b61f9ba..3775863 100644 --- a/src/db/sqlite/SqliteInterface.ts +++ b/src/db/sqlite/SqliteInterface.ts @@ -392,6 +392,13 @@ export class SqliteInterface implements BackendDbInterface { WHERE "uid" = ${{ uuid: uid }} `); } + async deleteSession(token: `as_${string}`): Promise { + this.#checkReadonly(); + this.#db.run(sql` + DELETE FROM "sessions" + WHERE "token" = ${{ text: token }} + `); + } #getApikeyByToken(token: `at_${string}`): ClientApikey { const apikey = selectClientApikey( diff --git a/src/db/types/DbInterface.ts b/src/db/types/DbInterface.ts index 44db796..c85dc75 100644 --- a/src/db/types/DbInterface.ts +++ b/src/db/types/DbInterface.ts @@ -85,6 +85,7 @@ export interface BackendDbInterface extends DbInterface { // auth by session getUserBySession(token: `as_${string}`): Promise; createSession(uid: string): Promise<`as_${string}`>; + deleteSession(token: `as_${string}`): Promise; // auth by apikey getUserByApikey(token: `at_${string}`): Promise<[ClientUser, ClientApikey]>; @@ -98,6 +99,7 @@ export function isBackendInterface(db: DbInterface): db is BackendDbInterface { "getUserByLogin", "getUserBySession", "createSession", + "deleteSession", "getUserByApikey", ] as const ).every( diff --git a/src/webapi/apirouter.ts b/src/webapi/apirouter.ts index be970cb..6c5c9ac 100644 --- a/src/webapi/apirouter.ts +++ b/src/webapi/apirouter.ts @@ -28,6 +28,8 @@ export function apirouter(db: BackendDbInterface): KoaRouter { }); router.post("/auth/logout", authenticate(db), async (ctx) => { if (ctx.session!.source !== "session") throw new InvalidAbodeError(); + const token = ctx.cookies.get("abode_session"); + if (token) await db.deleteSession(token as `as_${string}`); ctx.cookies.set("abode_session", "", { expires: new Date("1970-01-01") }); ctx.status = 204; }); diff --git a/test/backends/api/auth-http.test.ts b/test/backends/api/auth-http.test.ts index 9d76b2b..dbe287e 100644 --- a/test/backends/api/auth-http.test.ts +++ b/test/backends/api/auth-http.test.ts @@ -74,7 +74,7 @@ describe("api backend: auth over HTTP", async () => { assert.equal(res.status, 401); }); - it("POST /auth/logout clears the session cookie", async () => { + it("POST /auth/logout clears the cookie and invalidates the session server-side", async () => { const login = await fetch(`${server.url}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -88,6 +88,11 @@ describe("api backend: auth over HTTP", async () => { }); assert.equal(logout.status, 204); assert.equal(getCookie(logout, "abode_session"), ""); + + const self = await fetch(`${server.url}/auth/self`, { + headers: { Cookie: `abode_session=${cookie}` }, + }); + assert.equal(self.status, 401, "session was invalidated server-side, not just the cookie cleared"); }); it("POST /auth/clear-sessions invalidates outstanding session cookies", async () => { diff --git a/test/tools/authenticate.test.ts b/test/tools/authenticate.test.ts index 1a3e52d..6fa6a8a 100644 --- a/test/tools/authenticate.test.ts +++ b/test/tools/authenticate.test.ts @@ -71,6 +71,7 @@ function makeMockDb( getUserByLogin: async () => { throw new NotFoundAbodeError(); }, getUserBySession: async () => { throw new NotFoundAbodeError(); }, createSession: async () => `as_${"0".repeat(32)}`, + deleteSession: async () => {}, getUserByApikey: async () => { throw new NotFoundAbodeError(); }, ...overrides, };