Skip to content

Commit 1820693

Browse files
committed
fix(mobile): let API keys revoke themselves
- Allow API key authentication to revoke its own key for sign out - Prevent API keys from revoking other keys
1 parent f70cd78 commit 1820693

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

‎packages/trpc/routers/apiKeys.test.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,60 @@ describe("API Keys Routes", () => {
283283
unauthedAPICaller.apiKeys.revoke({ id: "some-id" }),
284284
).rejects.toThrow(/UNAUTHORIZED/);
285285
});
286+
287+
test<CustomTestContext>("an API key can revoke itself", async ({
288+
unauthedAPICaller,
289+
db,
290+
}) => {
291+
const user = await unauthedAPICaller.users.create({
292+
name: "Test User",
293+
email: "test@test.com",
294+
password: "password123",
295+
confirmPassword: "password123",
296+
});
297+
298+
const key = await unauthedAPICaller.apiKeys.exchange({
299+
keyName: "Mobile App",
300+
email: user.email,
301+
password: "password123",
302+
});
303+
const keyCaller = await getApiKeyCallerForPlainKey(db, key.key);
304+
305+
await keyCaller.apiKeys.revoke({ id: key.id });
306+
307+
const remainingKeys = await db
308+
.select()
309+
.from(apiKeys)
310+
.where(eq(apiKeys.id, key.id));
311+
expect(remainingKeys).toHaveLength(0);
312+
});
313+
314+
test<CustomTestContext>("an API key cannot revoke other keys", async ({
315+
unauthedAPICaller,
316+
db,
317+
}) => {
318+
const user = await unauthedAPICaller.users.create({
319+
name: "Test User",
320+
email: "test@test.com",
321+
password: "password123",
322+
confirmPassword: "password123",
323+
});
324+
325+
const api = getApiCaller(db, user.id, user.email).apiKeys;
326+
const otherKey = await api.create({ name: "Other Key" });
327+
const key = await api.create({ name: "Caller Key" });
328+
const keyCaller = await getApiKeyCallerForPlainKey(db, key.key);
329+
330+
await expect(
331+
keyCaller.apiKeys.revoke({ id: otherKey.id }),
332+
).rejects.toThrow(/API keys can only revoke themselves/);
333+
334+
const remainingKeys = await db
335+
.select()
336+
.from(apiKeys)
337+
.where(eq(apiKeys.id, otherKey.id));
338+
expect(remainingKeys).toHaveLength(1);
339+
});
286340
});
287341

288342
describe("validate", () => {

‎packages/trpc/routers/apiKeys.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
validatePassword,
1818
} from "../auth";
1919
import {
20+
authedProcedure,
2021
createEventLogMiddleware,
2122
createRateLimitMiddleware,
2223
publicProcedure,
@@ -86,7 +87,10 @@ export const apiKeysAppRouter = router({
8687
key: await regenerateApiKey(existingKey.id, ctx.user.id, ctx.db),
8788
};
8889
}),
89-
revoke: sessionProcedure
90+
// Sessions can revoke any of the user's keys. An API key can only revoke
91+
// itself, which is how the mobile app and the extension clean up their key
92+
// on sign out.
93+
revoke: authedProcedure
9094
.use(createEventLogMiddleware("apiKey.revoke"))
9195
.input(
9296
z.object({
@@ -95,6 +99,18 @@ export const apiKeysAppRouter = router({
9599
)
96100
.mutation(async ({ input, ctx }) => {
97101
addLogFields<"apiKey.revoke">({ "apiKey.id": input.id });
102+
if (ctx.auth?.type === "apiKey") {
103+
const key = await ctx.db.query.apiKeys.findFirst({
104+
where: and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.user.id)),
105+
columns: { keyId: true },
106+
});
107+
if (key?.keyId !== ctx.auth.keyId) {
108+
throw new TRPCError({
109+
code: "FORBIDDEN",
110+
message: "API keys can only revoke themselves",
111+
});
112+
}
113+
}
98114
await ctx.db
99115
.delete(apiKeys)
100116
.where(and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.user.id)));

0 commit comments

Comments
 (0)