File tree Expand file tree Collapse file tree
migrations/20251003172324_add_v2_push_notification_models Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -27,6 +27,9 @@ CREATE TABLE "ClientIdentifier" (
2727-- CreateIndex
2828CREATE INDEX "DeviceRegistration_pushToken_idx " ON " DeviceRegistration" (" pushToken" );
2929
30+ -- CreateIndex
31+ CREATE INDEX "DeviceRegistration_disabled_pushFailures_idx " ON " DeviceRegistration" (" disabled" , " pushFailures" );
32+
3033-- CreateIndex
3134CREATE INDEX "ClientIdentifier_deviceId_idx " ON " ClientIdentifier" (" deviceId" );
3235
Original file line number Diff line number Diff line change @@ -192,6 +192,7 @@ model DeviceRegistration {
192192 clientIdentifiers ClientIdentifier []
193193
194194 @@index ([pushToken ] )
195+ @@index ([disabled , pushFailures ] )
195196}
196197
197198model ClientIdentifier {
Original file line number Diff line number Diff line change 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
Original file line number Diff line number Diff line change @@ -13,6 +13,7 @@ import {
1313import { getHttpDeliveryNotificationAuthHeader } from "@/notifications/utils" ;
1414import { prisma } from "@/utils/prisma" ;
1515import { createV2JwtToken } from "@/utils/v2/jwt" ;
16+ import { MAX_PUSH_FAILURES } from "./constants" ;
1617
1718const notificationClient = createNotificationClient ( ) ;
1819const 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 (
Original file line number Diff line number Diff line change 11import { Router } from "express" ;
2- import { authV2Middleware } from "@/middleware/v2/auth" ;
32import { authRateLimitMiddleware } from "@/middleware/rateLimit" ;
3+ import { authV2Middleware } from "@/middleware/v2/auth" ;
44import { generateToken } from "./handlers/generate-token" ;
55
66const authRouter = Router ( ) ;
Original file line number Diff line number Diff line change @@ -3,6 +3,22 @@ import { z } from "zod";
33import { prisma } from "@/utils/prisma" ;
44import { 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+
622const generateTokenRequestSchema = z . object ( {
723 clientIdentifier : z . string ( ) ,
824 deviceId : z . string ( ) ,
Original file line number Diff line number Diff line change 11import * as jose from "jose" ;
2+ import { MAX_JWT_METADATA_SIZE } from "@/api/shared/notifications/constants" ;
23import { AppError } from "@/utils/errors" ;
34import logger from "@/utils/logger" ;
45import { 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 ,
You can’t perform that action at this time.
0 commit comments