From 7675867549f73db0df28e53fce1a7fdecae5c7eb Mon Sep 17 00:00:00 2001 From: Rushil Perera Date: Sat, 15 Jun 2024 05:47:26 -0400 Subject: [PATCH] feat: create lib function to verify FCM token --- bun.lockb | Bin 166116 -> 171957 bytes package.json | 1 + src/controllers/token/index.spec.ts | 1 - src/libs/fcm/getGoogleAuthToken.ts | 26 ++++++++++ src/libs/fcm/sendFcmMessage.ts | 73 ++++++++++++++++++++++++++++ src/libs/fcm/verifyFcm.spec.ts | 25 ++++++++++ src/libs/fcm/verifyFcmToken.ts | 26 ++++++++++ src/mocks/fcm.ts | 36 ++++++++++++++ src/mocks/gToken.ts | 30 ++++++++++++ src/mocks/handlers.ts | 2 + 10 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/libs/fcm/getGoogleAuthToken.ts create mode 100644 src/libs/fcm/sendFcmMessage.ts create mode 100644 src/libs/fcm/verifyFcm.spec.ts create mode 100644 src/libs/fcm/verifyFcmToken.ts create mode 100644 src/mocks/fcm.ts create mode 100644 src/mocks/gToken.ts diff --git a/bun.lockb b/bun.lockb index 2922ad38f282d3e7d868d7e0e72dc00d90b8377f..f805921612d2740ee537accf02934e831f081865 100755 GIT binary patch delta 34355 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"Y%#_3Q>xRGTF8cs*5`;P<{BA=Be@ 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 L=68$CFB|@UpFo;{ diff --git a/package.json b/package.json index c83bb48..fd9e107 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "drizzle-orm": "^0.31.2", "gql.tada": "^1.7.5", "graphql-request": "^7.0.1", + "gtoken": "^7.1.0", "hono": "^4.3.6", "zod": "^3.23.8" }, diff --git a/src/controllers/token/index.spec.ts b/src/controllers/token/index.spec.ts index 3a0994b..2c79ce9 100644 --- a/src/controllers/token/index.spec.ts +++ b/src/controllers/token/index.spec.ts @@ -174,7 +174,6 @@ describe("requests the /token route", () => { }); it("token already exists in db, should not insert new entry", async () => { - const minimumTimestamp = DateTime.now(); await db .insert(deviceTokensTable) .values({ deviceId: "123", token: "123" }); diff --git a/src/libs/fcm/getGoogleAuthToken.ts b/src/libs/fcm/getGoogleAuthToken.ts new file mode 100644 index 0000000..bfff89c --- /dev/null +++ b/src/libs/fcm/getGoogleAuthToken.ts @@ -0,0 +1,26 @@ +import { GoogleToken } from "gtoken"; + +export async function getGoogleAuthToken(adminSdkJson: AdminSdkKey) { + const { privateKey, clientEmail } = adminSdkJson; + + const gToken = new GoogleToken({ + key: privateKey, + email: clientEmail, + scope: ["https://www.googleapis.com/auth/firebase.messaging"], + }); + return gToken.getToken().then((token) => token.access_token); +} + +interface AdminSdkKey { + type: string; + projectID: string; + privateKeyID: string; + privateKey: string; + clientEmail: string; + clientID: string; + authURI: string; + tokenURI: string; + authProviderX509CERTURL: string; + clientX509CERTURL: string; + universeDomain: string; +} diff --git a/src/libs/fcm/sendFcmMessage.ts b/src/libs/fcm/sendFcmMessage.ts new file mode 100644 index 0000000..f360eb8 --- /dev/null +++ b/src/libs/fcm/sendFcmMessage.ts @@ -0,0 +1,73 @@ +import { getGoogleAuthToken } from "./getGoogleAuthToken"; + +export async function sendFcmMessage( + adminSdkJson: string, + message: FcmMessagePayload, + isOnlyValidatingFcmMessage?: boolean, +) { + return fetch( + "https://fcm.googleapis.com/v1/projects/aniplay-73b59/messages:send", + { + method: "POST", + body: JSON.stringify({ + message, + validate_only: isOnlyValidatingFcmMessage, + }), + headers: { + Authorization: `Bearer ${await getGoogleAuthToken(JSON.parse(adminSdkJson))}`, + }, + }, + ).then((res) => res.json()); +} + +type SendFcmMessageResponse = + | { error: SendFcmMessageError } + | FcmMessagePayload; + +interface SendFcmMessageError { + code: number; + message: string; + status: string; + details: Detail[]; +} + +export interface Detail { + type: string; + errorCode: string; +} + +export type FcmMessagePayload = { + name?: string; + data?: Record; + notification?: Notification; + android?: Partial; + webpush?: {}; + apns?: {}; + fcm_options?: {}; +} & ( + | { token: string; topic?: never; condition?: never } + | { token?: never; topic: string; condition?: never } + | { token?: never; topic?: never; condition: string } +); + +interface Notification { + title: string; + body: string; + image: string; +} + +interface AndroidConfig { + collapse_key: string; + priority: "normal" | "high"; + /** + * How long (in seconds) the message should be kept in FCM storage if the device is offline. The maximum time to live supported is 4 weeks, and the default value is 4 weeks if not set. Set it to 0 if want to send the message immediately. In JSON format, the Duration type is encoded as a string rather than an object, where the string ends in the suffix "s" (indicating seconds) and is preceded by the number of seconds, with nanoseconds expressed as fractional seconds. For example, 3 seconds with 0 nanoseconds should be encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should be expressed in JSON format as "3.000000001s". The ttl will be rounded down to the nearest second. + * + * A duration in seconds with up to nine fractional digits, ending with 's'. Example: "3.5s". + */ + ttl: string; + restricted_package_name: string; + data: Record; + notification: {}; + fcm_options: {}; + direct_boot_ok: boolean; +} diff --git a/src/libs/fcm/verifyFcm.spec.ts b/src/libs/fcm/verifyFcm.spec.ts new file mode 100644 index 0000000..0808227 --- /dev/null +++ b/src/libs/fcm/verifyFcm.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test"; + +import { server } from "~/mocks"; +import "~/mocks/gToken"; + +import { verifyFcmToken } from "./verifyFcmToken"; + +server.listen(); + +describe("verifyFcmToken", () => { + it("valid token, returns true", async () => { + const token = + "7v8sy43aq0re4r8xe7rmr0cn1fsmh6phehnfla2pa73z899zmhyarivmkt4sj6pyv0py43u6p2sim6wz2vg9ypjp9rug1keoth7f6ll3gdvas4q020u3ah51r6bjgn51j6bd92ztmtof3ljpcm8q31njvndy65enm68"; + const res = await verifyFcmToken(token, '{"clientEmail": "test@test.com"}'); + + expect(res).toBeTrue(); + }); + + it("invalid token, returns false", async () => { + const token = "abc123"; + const res = await verifyFcmToken(token, '{"clientEmail": "test@test.com"}'); + + expect(res).toBeFalse(); + }); +}); diff --git a/src/libs/fcm/verifyFcmToken.ts b/src/libs/fcm/verifyFcmToken.ts new file mode 100644 index 0000000..95c5c93 --- /dev/null +++ b/src/libs/fcm/verifyFcmToken.ts @@ -0,0 +1,26 @@ +import { sendFcmMessage } from "./sendFcmMessage"; + +export async function verifyFcmToken( + token: string, + adminSdkJson: string, +): Promise { + return sendFcmMessage( + adminSdkJson, + { name: "token_verification", token }, + true, + ) + .then((response) => { + const error = "error" in response ? response.error : undefined; + if (error) { + console.error("Received error response while validating FCM token"); + console.error(JSON.stringify(error)); + } + + return !error; + }) + .catch((err) => { + console.error("Failed to verify FCM token", err); + + return false; + }); +} diff --git a/src/mocks/fcm.ts b/src/mocks/fcm.ts new file mode 100644 index 0000000..9f9ae76 --- /dev/null +++ b/src/mocks/fcm.ts @@ -0,0 +1,36 @@ +import { HttpResponse, http } from "msw"; + +import type { FcmMessagePayload } from "~/libs/fcm/sendFcmMessage"; + +export function mockFcmMessageResponse() { + return http.post<{}, { message: FcmMessagePayload; validate_only: boolean }>( + "https://fcm.googleapis.com/v1/projects/aniplay-73b59/messages:send", + async ({ request }) => { + const { message } = await request.json(); + const { name, token } = message; + + if (name === "token_verification") { + if (token?.length === 163) { + return HttpResponse.json({ name }); + } + + return HttpResponse.json({ + error: { + code: 400, + message: + "The registration token is not a valid FCM registration token", + status: "INVALID_ARGUMENT", + details: [ + { + "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", + errorCode: "INVALID_ARGUMENT", + }, + ], + }, + }); + } + + return HttpResponse.json(message); + }, + ); +} diff --git a/src/mocks/gToken.ts b/src/mocks/gToken.ts new file mode 100644 index 0000000..1183635 --- /dev/null +++ b/src/mocks/gToken.ts @@ -0,0 +1,30 @@ +import type { TokenOptions } from "gtoken"; + +import { mock } from "bun:test"; + +const emailRegex = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +class MockGoogleToken { + private email: string | undefined; + + constructor(options: TokenOptions) { + this.email = options.email; + } + + getToken() { + if (!this.email) { + return Promise.reject("No email provided"); + } + + if (!emailRegex.test(this.email)) { + return Promise.reject("Invalid email"); + } + + return Promise.resolve({ + access_token: "asd", + }); + } +} + +mock.module("gtoken", () => ({ GoogleToken: MockGoogleToken })); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 928ff95..864982b 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -8,6 +8,7 @@ import { getAnifyTitle } from "./anify/title"; import { getAnilistSearchResults } from "./anilist/search"; import { getAnilistTitle } from "./anilist/title"; import { updateAnilistWatchStatus } from "./anilist/updateWatchStatus"; +import { mockFcmMessageResponse } from "./fcm"; export const handlers = [ getAnilistSearchResults(), @@ -20,4 +21,5 @@ export const handlers = [ getAnifyEpisodes(), getAnifySources(), getAnifyTitle(), + mockFcmMessageResponse(), ];