Browse Source

Updates and a remaining authentication error to the push service at Mozilla

unsubscribe-mute
Matthew Raymer 1 year ago
parent
commit
5570c0e3dd
  1. 19170
      package-lock.json
  2. 81
      package.json
  3. 1
      src/main.ts
  4. 27
      src/notificationService.ts
  5. 17
      src/vapidService.ts

19170
package-lock.json

File diff suppressed because it is too large

81
package.json

@ -4,55 +4,56 @@
"description": "Minimalistic boilerplate to quick-start Node.js development in TypeScript.", "description": "Minimalistic boilerplate to quick-start Node.js development in TypeScript.",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">= 18.12 <19" "node": ">= 18.12 <19"
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "~29.5", "@types/jest": "~29.5",
"@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2",
"@types/node": "~20", "@types/node": "~20",
"@types/sqlite3": "^3.1.8", "@types/sqlite3": "^3.1.8",
"@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0", "@typescript-eslint/parser": "^6.4.0",
"eslint": "~8.47", "eslint": "~8.47",
"eslint-config-prettier": "~9.0", "eslint-config-prettier": "~9.0",
"eslint-plugin-jest": "~27.2", "eslint-plugin-jest": "~27.2",
"jest": "~29.6", "jest": "~29.6",
"prettier": "~3.0", "prettier": "~3.0",
"rimraf": "~5.0", "rimraf": "~5.0",
"ts-api-utils": "~1.0", "ts-api-utils": "~1.0",
"ts-jest": "~29.1", "ts-jest": "~29.1",
"typescript": "~5.1" "typescript": "~5.1"
}, },
"scripts": { "scripts": {
"start": "node build/src/main.js", "start": "node build/src/main.js",
"clean": "rimraf coverage build tmp", "clean": "rimraf coverage build tmp",
"prebuild": "npm run lint", "prebuild": "npm run lint",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"build:watch": "tsc -w -p tsconfig.json", "build:watch": "tsc -w -p tsconfig.json",
"build:release": "npm run clean && tsc -p tsconfig.release.json", "build:release": "npm run clean && tsc -p tsconfig.release.json",
"lint": "eslint . --ext .ts --ext .mts", "lint": "eslint . --ext .ts --ext .mts",
"test": "jest --coverage", "test": "jest --coverage",
"prettier": "prettier --config .prettierrc --write .", "prettier": "prettier --config .prettierrc --write .",
"test:watch": "jest --watch" "test:watch": "jest --watch"
}, },
"author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>", "author": "Jakub Synowiec <jsynowiec@users.noreply.github.com>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"elliptic": "^6.5.4", "eckey-utils": "^0.7.13",
"express": "^4.18.2", "elliptic": "^6.5.4",
"http_ece": "^1.1.0", "express": "^4.18.2",
"jsonwebtoken": "^9.0.1", "http_ece": "^1.1.0",
"node-fetch": "^3.3.2", "jsonwebtoken": "^9.0.1",
"npm-check-updates": "16.11.1", "node-fetch": "^3.3.2",
"reflect-metadata": "^0.1.13", "npm-check-updates": "16.11.1",
"sqlite3": "^5.1.6", "reflect-metadata": "^0.1.13",
"tslib": "~2.6", "sqlite3": "^5.1.6",
"typeorm": "^0.3.17" "tslib": "~2.6",
"typeorm": "^0.3.17"
}, },
"volta": { "volta": {
"node": "18.12.1" "node": "18.12.1"
} }
} }

1
src/main.ts

@ -46,7 +46,6 @@ class Server {
this.app.post('/web-push/subscribe', async (req: Request, res: Response) => { this.app.post('/web-push/subscribe', async (req: Request, res: Response) => {
const subscription = req.body as BrowserSubscription; const subscription = req.body as BrowserSubscription;
const message = { "title": "You are subscribed." } as Message; const message = { "title": "You are subscribed." } as Message;
console.log("/web-push/subscribe", req.body);
await this.subscriptionService.addSubscription(subscription); await this.subscriptionService.addSubscription(subscription);
await this.notificationService.sendNotification(subscription, message); await this.notificationService.sendNotification(subscription, message);
res.status(201).send(); res.status(201).send();

27
src/notificationService.ts

@ -42,11 +42,13 @@ export class NotificationService {
private async pushToEndpoint(subscription: BrowserSubscription, message: Message): Promise<void> { private async pushToEndpoint(subscription: BrowserSubscription, message: Message): Promise<void> {
const payloadString = JSON.stringify(message); const payloadString = JSON.stringify(message);
const payloadBuffer = Buffer.from(payloadString, 'utf-8'); const payloadBuffer = Buffer.from(payloadString, 'utf-8');
const vapidKeys: VapidKeys[] = await this.vapidService.getVapidKeys();
const vapidkey: VapidKeys = vapidKeys[0];
const encrypted = await this.encrypt(subscription.keys.p256dh, subscription.keys.auth, payloadBuffer); const encrypted = await this.encrypt(vapidkey, subscription.keys.p256dh, subscription.keys.auth, payloadBuffer);
const endpoint = subscription.endpoint; const endpoint = subscription.endpoint;
const vapidHeaders = await this.vapidService.createVapidAuthHeader(endpoint, 12 * 60 * 60, 'mailto:example@example.com'); const vapidHeaders = await this.vapidService.createVapidAuthHeader(endpoint, 12 * 60 * 60, 'mailto:example@example.com', vapidkey);
const parsedUrl = new URL(subscription.endpoint); const parsedUrl = new URL(subscription.endpoint);
const options: https.RequestOptions = { const options: https.RequestOptions = {
@ -63,6 +65,9 @@ export class NotificationService {
}, },
}; };
console.log(parsedUrl);
console.log(options);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = https.request(options, (res) => { const req = https.request(options, (res) => {
if (res.statusCode! >= 200 && res.statusCode! < 300) { if (res.statusCode! >= 200 && res.statusCode! < 300) {
@ -82,34 +87,22 @@ export class NotificationService {
} }
private async encrypt(p256dh: string, auth: string, payload: Buffer): Promise<Buffer> { private async encrypt(appKeys: VapidKeys, p256dh: string, auth: string, payload: Buffer): Promise<Buffer> {
try { try {
console.log('Public Key:', p256dh); const vapidPrivateKeyBase64: string = appKeys['privateKey'];
console.log('Auth:', auth);
const vapidKeys: VapidKeys[] = await this.vapidService.getVapidKeys();
const vapidkey: VapidKeys = vapidKeys[0];
const vapidPrivateKeyBase64: string = vapidkey['privateKey'];
console.log("vapidPrivateKeyBase64: ", vapidPrivateKeyBase64);
const vapidPrivateKeyBuffer: Buffer = Buffer.from(vapidPrivateKeyBase64, 'base64'); const vapidPrivateKeyBuffer: Buffer = Buffer.from(vapidPrivateKeyBase64, 'base64');
const ecdh = crypto.createECDH('prime256v1'); const ecdh = crypto.createECDH('prime256v1');
ecdh.setPrivateKey(vapidPrivateKeyBuffer); ecdh.setPrivateKey(vapidPrivateKeyBuffer);
const publicKeyBuffer: Buffer = Buffer.from(p256dh, 'base64'); const publicKeyBuffer: Buffer = Buffer.from(p256dh, 'base64');
console.log("1: ", payload);
console.log("3: ", publicKeyBuffer);
console.log("4: ", auth);
return http_ece.encrypt(payload, { return http_ece.encrypt(payload, {
'privateKey': ecdh, 'privateKey': ecdh,
'dh': publicKeyBuffer, 'dh': publicKeyBuffer,
'authSecret': Buffer.from(auth) 'authSecret': Buffer.from(auth)
}); });
} catch (error) { } catch (error) {
console.error('Error encrypting payload:', error); console.error('Error encrypting payload:', error);
// Handle the error as appropriate for your application
throw error; throw error;
} }
} }

17
src/vapidService.ts

@ -1,4 +1,4 @@
import ecKeyUtils from "eckey-utils";
import { VapidKeys } from './VapidKeys.js'; import { VapidKeys } from './VapidKeys.js';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import crypto from 'crypto'; import crypto from 'crypto';
@ -16,7 +16,6 @@ class VapidService {
private static instance: VapidService; private static instance: VapidService;
private dbService: DBService = DBService.getInstance(); private dbService: DBService = DBService.getInstance();
private constructor() { private constructor() {
} }
@ -60,16 +59,22 @@ class VapidService {
} }
async createVapidAuthHeader(endpoint: string, expiration: number, subject: string): Promise<{ 'Authorization': string, 'Crypto-Key': string }> { async createVapidAuthHeader(endpoint: string, expiration: number, subject: string, appKeys: VapidKeys): Promise<{ 'Authorization': string, 'Crypto-Key': string }> {
const { publicKey, privateKey } = await this.getVapidKeys()[0]; const { publicKey, privateKey } = appKeys;
const jwtInfo = { const jwtInfo = {
aud: new URL(endpoint).origin, aud: new URL(endpoint).origin,
exp: Math.floor((Date.now() / 1000) + expiration), exp: Math.floor((Date.now() / 1000) + expiration),
sub: subject sub: subject
}; };
const curveName = 'prime256v1';
const jwtToken = jwt.sign(jwtInfo, privateKey, { algorithm: 'ES256' }); const ecdh = crypto.createECDH(curveName);
const privateKeyBuffer = Buffer.from(privateKey, 'base64');
ecdh.setPrivateKey(privateKeyBuffer);
const pems = ecKeyUtils.generatePem({curveName, privateKey: ecdh.getPrivateKey(), publicKey: ecdh.getPublicKey() });
const jwtToken = jwt.sign(jwtInfo, pems.privateKey, { algorithm: 'ES256' });
return { return {
'Authorization': `vapid t=${jwtToken}, k=${publicKey}`, 'Authorization': `vapid t=${jwtToken}, k=${publicKey}`,

Loading…
Cancel
Save