Skip to content

Commit 7fed80a

Browse files
feedback
1 parent 76c68af commit 7fed80a

File tree

3 files changed

+25
-11
lines changed

3 files changed

+25
-11
lines changed

packages/web/src/ee/features/oauth/server.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,21 @@ describe('verifyAndRotateRefreshToken', () => {
212212
});
213213
});
214214

215-
test('returns invalid_grant and revokes all tokens when refresh token is not found (theft detection)', async () => {
215+
test('returns invalid_grant immediately if token does not have the correct prefix', async () => {
216+
const result = await verifyAndRotateRefreshToken({
217+
rawRefreshToken: 'invalid_prefix_token',
218+
clientId: 'test-client-id',
219+
resource: null,
220+
});
221+
222+
expect(result).toMatchObject({ error: 'invalid_grant' });
223+
expect(prisma.oAuthRefreshToken.findUnique).not.toHaveBeenCalled();
224+
expect(prisma.oAuthToken.deleteMany).not.toHaveBeenCalled();
225+
expect(prisma.oAuthRefreshToken.deleteMany).not.toHaveBeenCalled();
226+
});
227+
228+
test('returns invalid_grant when refresh token is not found without revoking other tokens', async () => {
216229
prisma.oAuthRefreshToken.findUnique.mockResolvedValue(null);
217-
prisma.oAuthToken.deleteMany.mockResolvedValue({ count: 1 });
218-
prisma.oAuthRefreshToken.deleteMany.mockResolvedValue({ count: 1 });
219230

220231
const result = await verifyAndRotateRefreshToken({
221232
rawRefreshToken: 'sbor_used',
@@ -224,8 +235,8 @@ describe('verifyAndRotateRefreshToken', () => {
224235
});
225236

226237
expect(result).toMatchObject({ error: 'invalid_grant' });
227-
expect(prisma.oAuthToken.deleteMany).toHaveBeenCalledWith({ where: { clientId: 'test-client-id' } });
228-
expect(prisma.oAuthRefreshToken.deleteMany).toHaveBeenCalledWith({ where: { clientId: 'test-client-id' } });
238+
expect(prisma.oAuthToken.deleteMany).not.toHaveBeenCalled();
239+
expect(prisma.oAuthRefreshToken.deleteMany).not.toHaveBeenCalled();
229240
});
230241

231242
test('returns invalid_grant if client_id does not match', async () => {

packages/web/src/ee/features/oauth/server.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,17 +153,15 @@ export async function verifyAndRotateRefreshToken({
153153
clientId: string;
154154
resource: string | null;
155155
}): Promise<{ token: string; refreshToken: string; expiresIn: number } | { error: string; errorDescription: string }> {
156+
if (!rawRefreshToken.startsWith(OAUTH_REFRESH_TOKEN_PREFIX)) {
157+
return { error: 'invalid_grant', errorDescription: 'Refresh token is invalid.' };
158+
}
159+
156160
const hash = hashSecret(rawRefreshToken.slice(OAUTH_REFRESH_TOKEN_PREFIX.length));
157161

158162
const existing = await prisma.oAuthRefreshToken.findUnique({ where: { hash } });
159163

160164
if (!existing) {
161-
// Token not found — either never existed or already rotated.
162-
// Treat as potential theft: revoke all tokens for this client to be safe.
163-
await prisma.$transaction([
164-
prisma.oAuthToken.deleteMany({ where: { clientId } }),
165-
prisma.oAuthRefreshToken.deleteMany({ where: { clientId } }),
166-
]);
167165
return { error: 'invalid_grant', errorDescription: 'Refresh token is invalid or has already been used.' };
168166
}
169167

packages/web/src/withAuthV2.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,23 +196,28 @@ describe('getAuthenticatedUser', () => {
196196
});
197197

198198
test('should return undefined if an OAuth Bearer token is present but the token does not exist', async () => {
199+
mocks.hasEntitlement.mockReturnValue(true);
199200
prisma.oAuthToken.findUnique.mockResolvedValue(null);
200201
setMockHeaders(new Headers({ 'Authorization': 'Bearer sboa_oauthtoken' }));
201202
const user = await getAuthenticatedUser();
202203
expect(user).toBeUndefined();
204+
expect(prisma.oAuthToken.findUnique).toHaveBeenCalled();
203205
});
204206

205207
test('should return undefined if an OAuth Bearer token is present but the token is expired', async () => {
208+
mocks.hasEntitlement.mockReturnValue(true);
206209
prisma.oAuthToken.findUnique.mockResolvedValue({
207210
...MOCK_OAUTH_TOKEN,
208211
expiresAt: new Date(Date.now() - 1000), // expired 1 second ago
209212
});
210213
setMockHeaders(new Headers({ 'Authorization': 'Bearer sboa_oauthtoken' }));
211214
const user = await getAuthenticatedUser();
212215
expect(user).toBeUndefined();
216+
expect(prisma.oAuthToken.findUnique).toHaveBeenCalled();
213217
});
214218

215219
test('should not check API key when a sboa_ Bearer token is present', async () => {
220+
mocks.hasEntitlement.mockReturnValue(true);
216221
prisma.oAuthToken.findUnique.mockResolvedValue(MOCK_OAUTH_TOKEN);
217222
setMockHeaders(new Headers({ 'Authorization': 'Bearer sboa_oauthtoken' }));
218223
await getAuthenticatedUser();

0 commit comments

Comments
 (0)