Skip to content

Commit 5fd3acc

Browse files
abdulhannanalimarco-c
authored andcommitted
Add option to add Custom Expiration (#287)
* add custom expiration option * export `validateExpiration` * resolve nits for custom expiration * add exceeding expiration value test
1 parent 217f841 commit 5fd3acc

File tree

2 files changed

+86
-6
lines changed

2 files changed

+86
-6
lines changed

src/vapid-helper.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ const asn1 = require('asn1.js');
66
const jws = require('jws');
77
const url = require('url');
88

9+
/**
10+
* DEFAULT_EXPIRATION is set to seconds in 12 hours
11+
*/
12+
const DEFAULT_EXPIRATION_SECONDS = 12 * 60 * 60;
13+
14+
// Maximum expiration is 24 hours according. (See VAPID spec)
15+
const MAX_EXPIRATION_SECONDS = 24 * 60 * 60;
16+
917
const ECPrivateKeyASN = asn1.define('ECPrivateKey', function() {
1018
this.seq().obj(
1119
this.key('version').int(),
@@ -89,6 +97,44 @@ function validatePrivateKey(privateKey) {
8997
}
9098
}
9199

100+
/**
101+
* Given the number of seconds calculates
102+
* the expiration in the future by adding the passed `numSeconds`
103+
* with the current seconds from Unix Epoch
104+
*
105+
* @param {Number} numSeconds Number of seconds to be added
106+
* @return {Number} Future expiration in seconds
107+
*/
108+
function getFutureExpirationTimestamp(numSeconds) {
109+
const futureExp = new Date();
110+
futureExp.setSeconds(futureExp.getSeconds() + numSeconds);
111+
return Math.floor(futureExp.getTime() / 1000);
112+
}
113+
114+
/**
115+
* Validates the Expiration Header based on the VAPID Spec
116+
* Throws error of type `Error` if the expiration is not validated
117+
*
118+
* @param {Number} expiration Expiration seconds from Epoch to be validated
119+
*/
120+
function validateExpiration(expiration) {
121+
if (!Number.isInteger(expiration)) {
122+
throw new Error('`expiration` value must be a number');
123+
}
124+
125+
if (expiration < 0) {
126+
throw new Error('`expiration` must be a positive integer');
127+
}
128+
129+
// Roughly checks the time of expiration, since the max expiration can be ahead
130+
// of the time than at the moment the expiration was generated
131+
const maxExpirationTimestamp = getFutureExpirationTimestamp(MAX_EXPIRATION_SECONDS);
132+
133+
if (expiration >= maxExpirationTimestamp) {
134+
throw new Error('`expiration` value is greater than maximum of 24 hours');
135+
}
136+
}
137+
92138
/**
93139
* This method takes the required VAPID parameters and returns the required
94140
* header to be added to a Web Push Protocol Request.
@@ -123,11 +169,10 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
123169
publicKey = urlBase64.decode(publicKey);
124170
privateKey = urlBase64.decode(privateKey);
125171

126-
const DEFAULT_EXPIRATION = Math.floor(Date.now() / 1000) + 43200;
127-
128172
if (expiration) {
129-
// TODO: Check if expiration is valid and use it in place of the hard coded
130-
// expiration of 24hours.
173+
validateExpiration(expiration);
174+
} else {
175+
expiration = getFutureExpirationTimestamp(DEFAULT_EXPIRATION_SECONDS);
131176
}
132177

133178
const header = {
@@ -137,7 +182,7 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
137182

138183
const jwtPayload = {
139184
aud: audience,
140-
exp: DEFAULT_EXPIRATION,
185+
exp: expiration,
141186
sub: subject
142187
};
143188

@@ -155,8 +200,10 @@ function getVapidHeaders(audience, subject, publicKey, privateKey, expiration) {
155200

156201
module.exports = {
157202
generateVAPIDKeys: generateVAPIDKeys,
203+
getFutureExpirationTimestamp: getFutureExpirationTimestamp,
158204
getVapidHeaders: getVapidHeaders,
159205
validateSubject: validateSubject,
160206
validatePublicKey: validatePublicKey,
161-
validatePrivateKey: validatePrivateKey
207+
validatePrivateKey: validatePrivateKey,
208+
validateExpiration: validateExpiration
162209
};

test/test-vapid-helper.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,27 @@ suite('Test Vapid Helpers', function() {
8585
},
8686
function() {
8787
vapidHelper.getVapidHeaders(VALID_AUDIENCE, { something: 'else' }, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
88+
},
89+
function () {
90+
// String with text, is not accepted as a valid expiration value
91+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, 'Not valid expiration: Must be a number, this is a string with text');
92+
},
93+
function () {
94+
// Object is not accepted as a valid expiration value
95+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, { message: 'Not valid expiration: Must be a number, this is an object' });
96+
},
97+
function () {
98+
// Boolean is not accepted as a valid expiration value
99+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, true);
100+
},
101+
function () {
102+
// String is not accepted as a valid expiration value
103+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, '12213');
104+
},
105+
function () {
106+
// Invalid `expiration` as it exceeds 24 hours in duration
107+
const invalidExpiration = Math.floor(Date.now() / 1000) + (25 * 60 * 60);
108+
vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, invalidExpiration);
88109
}
89110
];
90111

@@ -109,6 +130,18 @@ suite('Test Vapid Helpers', function() {
109130
function() {
110131
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL,
111132
VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, VALID_EXPIRATION);
133+
},
134+
function() {
135+
// 0 is a valid value for `expiration`
136+
// since the the `expiration` value isn't checked for minimum
137+
const secondsFromEpoch = 0;
138+
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, secondsFromEpoch);
139+
},
140+
function () {
141+
// Valid value for `secondsFromEpoch` passed in to
142+
// `vapidHelper.getVapidHeaders` function
143+
const secondsFromEpoch = Math.floor(Date.now() / 1000) + (5 * 60 * 60);
144+
return vapidHelper.getVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY, secondsFromEpoch);
112145
}
113146
];
114147

0 commit comments

Comments
 (0)