Skip to content

Commit 5ac44ca

Browse files
committed
Fixes
1 parent cdbc1f1 commit 5ac44ca

7 files changed

Lines changed: 48 additions & 3 deletions

File tree

prisma/migrations/20251003172324_add_v2_push_notification_models/migration.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ CREATE TABLE "ClientIdentifier" (
2727
-- CreateIndex
2828
CREATE INDEX "DeviceRegistration_pushToken_idx" ON "DeviceRegistration"("pushToken");
2929

30+
-- CreateIndex
31+
CREATE INDEX "DeviceRegistration_disabled_pushFailures_idx" ON "DeviceRegistration"("disabled", "pushFailures");
32+
3033
-- CreateIndex
3134
CREATE INDEX "ClientIdentifier_deviceId_idx" ON "ClientIdentifier"("deviceId");
3235

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ model DeviceRegistration {
192192
clientIdentifiers ClientIdentifier[]
193193
194194
@@index([pushToken])
195+
@@index([disabled, pushFailures])
195196
}
196197

197198
model ClientIdentifier {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Maximum number of consecutive push notification failures before disabling a device
3+
*/
4+
export const MAX_PUSH_FAILURES = 10;
5+
6+
/**
7+
* Maximum size in bytes for JWT metadata to prevent token bloat
8+
*/
9+
export const MAX_JWT_METADATA_SIZE = 1024; // 1KB

src/api/shared/notifications/webhook-handler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { getHttpDeliveryNotificationAuthHeader } from "@/notifications/utils";
1414
import { prisma } from "@/utils/prisma";
1515
import { createV2JwtToken } from "@/utils/v2/jwt";
16+
import { MAX_PUSH_FAILURES } from "./constants";
1617

1718
const notificationClient = createNotificationClient();
1819
const pushNotificationService = getPushNotificationService();
@@ -186,7 +187,10 @@ async function handleV2Notification(args: {
186187
const { notification, client, req } = args;
187188

188189
// Check if device is disabled or has too many failures
189-
if (client.device.disabled || client.device.pushFailures >= 10) {
190+
if (
191+
client.device.disabled ||
192+
client.device.pushFailures >= MAX_PUSH_FAILURES
193+
) {
190194
req.log.warn(
191195
`Device ${client.deviceId} is disabled or has too many failures. Skipping notification.`,
192196
);
@@ -264,7 +268,7 @@ async function handleV2Notification(args: {
264268
data: {
265269
pushFailures: newFailureCount,
266270
lastFailureAt: new Date(),
267-
disabled: newFailureCount >= 10,
271+
disabled: newFailureCount >= MAX_PUSH_FAILURES,
268272
},
269273
});
270274
req.log.warn(

src/api/v2/auth/auth.router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Router } from "express";
2-
import { authV2Middleware } from "@/middleware/v2/auth";
32
import { authRateLimitMiddleware } from "@/middleware/rateLimit";
3+
import { authV2Middleware } from "@/middleware/v2/auth";
44
import { generateToken } from "./handlers/generate-token";
55

66
const authRouter = Router();

src/api/v2/auth/handlers/generate-token.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ import { z } from "zod";
33
import { prisma } from "@/utils/prisma";
44
import { createV2JwtToken } from "@/utils/v2/jwt";
55

6+
/**
7+
* Token Generation Security Model
8+
*
9+
* This endpoint generates short-lived JWT tokens for Gateway authentication:
10+
*
11+
* 1. The outer authV2Middleware validates the request using Firebase AppCheck,
12+
* which verifies the request originates from a legitimate app instance.
13+
* 2. AppCheck validation is sufficient to prove device ownership - if a device
14+
* passes AppCheck, it's trusted to request tokens for any client identifier
15+
* associated with that device.
16+
* 3. The clientIdentifier->deviceId mapping is validated in the database to
17+
* ensure the client belongs to the requesting device.
18+
* 4. Rate limiting (10 requests per 15 minutes) prevents token exhaustion attacks.
19+
* 5. Tokens are short-lived (15 minutes) to limit exposure window.
20+
*/
21+
622
const generateTokenRequestSchema = z.object({
723
clientIdentifier: z.string(),
824
deviceId: z.string(),

src/utils/v2/jwt.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as jose from "jose";
2+
import { MAX_JWT_METADATA_SIZE } from "@/api/shared/notifications/constants";
23
import { AppError } from "@/utils/errors";
34
import logger from "@/utils/logger";
45
import { tryCatch } from "@/utils/try-catch";
@@ -20,6 +21,17 @@ export const createV2JwtToken = async (args: {
2021
metadata?: V2JWTMetadata;
2122
expirationTime?: string;
2223
}) => {
24+
// Validate metadata size to prevent JWT bloat
25+
if (args.metadata) {
26+
const metadataSize = JSON.stringify(args.metadata).length;
27+
if (metadataSize > MAX_JWT_METADATA_SIZE) {
28+
throw new AppError(
29+
400,
30+
`JWT metadata exceeds maximum size of ${MAX_JWT_METADATA_SIZE} bytes`,
31+
);
32+
}
33+
}
34+
2335
const payload: V2JWTPayload = {
2436
clientIdentifier: args.clientIdentifier,
2537
deviceId: args.deviceId,

0 commit comments

Comments
 (0)