From 23d4cc86f4751f1081a76c22c1ea0d95ab2de05d Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:13:08 +0000 Subject: [PATCH] fix(oauth): reject access tokens and consent at expiration (#2662) --- .../src/oauth/__tests__/oauth.test.ts | 69 +++++++++++++++++++ packages/services/src/oauth/session.ts | 4 +- packages/services/src/oauth/verify.ts | 2 +- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/services/src/oauth/__tests__/oauth.test.ts b/packages/services/src/oauth/__tests__/oauth.test.ts index 5b11fdec..cdc14e37 100644 --- a/packages/services/src/oauth/__tests__/oauth.test.ts +++ b/packages/services/src/oauth/__tests__/oauth.test.ts @@ -282,6 +282,41 @@ describe("createSession", () => { }); describe("getSession", () => { + test("rejects reads and decisions at the expiration instant", async () => { + await withTestTransaction(async (tx) => { + const client = await register(tx); + const { id } = await start(tx, client.client_id); + const { expiresAt } = await getSession({ input: { id }, db: tx }); + expect( + ( + await getSession({ + input: { id }, + db: tx, + now: new Date(expiresAt.getTime() - 1), + }) + ).id, + ).toBe(id); + for (const delta of [0, 1]) { + const now = new Date(expiresAt.getTime() + delta); + await expect( + getSession({ input: { id }, db: tx, now }), + ).rejects.toBeInstanceOf(PreconditionFailedError); + await expect( + decideSession({ + input: { + id, + approved: true, + userId: ownerId, + workspaceId: team.id, + }, + db: tx, + now, + }), + ).rejects.toBeInstanceOf(PreconditionFailedError); + } + }); + }); + test("returns client name and requested scope while pending", async () => { await withTestTransaction(async (tx) => { const client = await register(tx, "Cursor"); @@ -800,6 +835,40 @@ describe("exchangeCode", () => { }); describe("verifyAccessToken", () => { + test("rejects access tokens at the expiration instant", async () => { + await withTestTransaction(async (tx) => { + const client = await register(tx); + const tokens = await mintGrant(tx, { + clientId: client.client_id, + userId: ownerId, + workspaceId: team.id, + }); + const grantId = await grantIdOf(tx, tokens.access_token); + const grant = await tx + .select() + .from(oauthGrant) + .where(eq(oauthGrant.id, grantId)) + .get(); + if (!grant) throw new Error("grant missing"); + expect( + ( + await verifyAccessToken(tokens.access_token, { + db: tx, + now: new Date(grant.accessTokenExpiresAt.getTime() - 1), + }) + )?.grantId, + ).toBe(grantId); + for (const delta of [0, 1]) { + expect( + await verifyAccessToken(tokens.access_token, { + db: tx, + now: new Date(grant.accessTokenExpiresAt.getTime() + delta), + }), + ).toBeNull(); + } + }); + }); + test("resolves workspace, user and scopes and bumps last_used_at", async () => { await withTestTransaction(async (tx) => { const client = await register(tx); diff --git a/packages/services/src/oauth/session.ts b/packages/services/src/oauth/session.ts index 6e9726b8..883d129d 100644 --- a/packages/services/src/oauth/session.ts +++ b/packages/services/src/oauth/session.ts @@ -1,7 +1,7 @@ import { db as defaultDb, eq } from "@openstatus/db"; import { - type OAuthSession, oauthClient, + type OAuthSession, oauthSession, selectOAuthSessionSchema, } from "@openstatus/db/src/schema"; @@ -154,7 +154,7 @@ export async function loadPendingSession( "This authorization request was already answered", ); } - if (session.expiresAt < now) { + if (session.expiresAt <= now) { throw new PreconditionFailedError("This authorization request expired"); } return session; diff --git a/packages/services/src/oauth/verify.ts b/packages/services/src/oauth/verify.ts index 32db9912..f21c96b2 100644 --- a/packages/services/src/oauth/verify.ts +++ b/packages/services/src/oauth/verify.ts @@ -37,7 +37,7 @@ export async function verifyAccessToken( .where(eq(oauthGrant.accessTokenHash, await sha256Hex(token))) .get(); if (!grant || grant.revokedAt) return null; - if (grant.accessTokenExpiresAt < now) return null; + if (grant.accessTokenExpiresAt <= now) return null; if (shouldUpdateLastUsed(grant.lastUsedAt, undefined, now)) { await db -- 2.51.2