diff --git a/src/jwt/produce.ts b/src/jwt/produce.ts index 0c32f093b4..e6d356336a 100644 --- a/src/jwt/produce.ts +++ b/src/jwt/produce.ts @@ -105,11 +105,16 @@ export class ProduceJWT { * @param input "iat" (Issued At) Claim value to set on the JWT Claims Set. Default is current * timestamp. */ - setIssuedAt(input?: number | Date) { + setIssuedAt(input?: number | string | Date) { if (typeof input === 'undefined') { this._payload = { ...this._payload, iat: epoch(new Date()) } } else if (input instanceof Date) { this._payload = { ...this._payload, iat: validateInput('setIssuedAt', epoch(input)) } + } else if (typeof input === 'string') { + this._payload = { + ...this._payload, + iat: validateInput('setIssuedAt', epoch(new Date()) + secs(input)), + } } else { this._payload = { ...this._payload, iat: validateInput('setIssuedAt', input) } } diff --git a/src/lib/secs.ts b/src/lib/secs.ts index 8d9f07f7de..0abc825f8c 100644 --- a/src/lib/secs.ts +++ b/src/lib/secs.ts @@ -5,17 +5,19 @@ const week = day * 7 const year = day * 365.25 const REGEX = - /^(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)$/i + /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i export default (str: string): number => { const matched = REGEX.exec(str) - if (!matched) { + if (!matched || (matched[4] && matched[1])) { throw new TypeError('Invalid time period format') } - const value = parseFloat(matched[1]) - const unit = matched[2].toLowerCase() + const value = parseFloat(matched[2]) + const unit = matched[3].toLowerCase() + + let numericDate: number switch (unit) { case 'sec': @@ -23,29 +25,41 @@ export default (str: string): number => { case 'second': case 'seconds': case 's': - return Math.round(value) + numericDate = Math.round(value) + break case 'minute': case 'minutes': case 'min': case 'mins': case 'm': - return Math.round(value * minute) + numericDate = Math.round(value * minute) + break case 'hour': case 'hours': case 'hr': case 'hrs': case 'h': - return Math.round(value * hour) + numericDate = Math.round(value * hour) + break case 'day': case 'days': case 'd': - return Math.round(value * day) + numericDate = Math.round(value * day) + break case 'week': case 'weeks': case 'w': - return Math.round(value * week) + numericDate = Math.round(value * week) + break // years matched default: - return Math.round(value * year) + numericDate = Math.round(value * year) + break + } + + if (matched[1] === '-' || matched[4] === 'ago') { + return -numericDate } + + return numericDate } diff --git a/test/jwt/encrypt.test.mjs b/test/jwt/encrypt.test.mjs index ce1d74d5ee..3c040211a0 100644 --- a/test/jwt/encrypt.test.mjs +++ b/test/jwt/encrypt.test.mjs @@ -1,5 +1,6 @@ import test from 'ava' import timekeeper from 'timekeeper' +import { setters } from './time_setters.mjs' const { EncryptJWT, compactDecrypt, jwtDecrypt } = await import('#dist') @@ -102,21 +103,19 @@ async function testJWTsetFunction(t, method, claim, value, duplicate = false, ex } } testJWTsetFunction.title = (title, method, claim, value) => - `EncryptJWT.prototype.${method} called with ${value}${title ? ` (${title})` : ''}` + `EncryptJWT.prototype.${method} called with ${ + value?.constructor?.name || typeof value + } (${value}) ${title ? ` (${title})` : ''}` + +for (const [method, claim, vectors] of setters(now)) { + for (const [input, output = input] of vectors) { + test(testJWTsetFunction, method, claim, input, false, output) + } +} -test(testJWTsetFunction, 'setIssuer', 'iss', 'urn:example:issuer') test('duplicated', testJWTsetFunction, 'setIssuer', 'iss', 'urn:example:issuer', true) -test(testJWTsetFunction, 'setSubject', 'sub', 'urn:example:subject') test('duplicated', testJWTsetFunction, 'setSubject', 'sub', 'urn:example:subject', true) -test(testJWTsetFunction, 'setAudience', 'aud', 'urn:example:audience') test('duplicated', testJWTsetFunction, 'setAudience', 'aud', 'urn:example:audience', true) -test(testJWTsetFunction, 'setJti', 'jti', 'urn:example:jti') -test(testJWTsetFunction, 'setIssuedAt', 'iat', 0) -test(testJWTsetFunction, 'setIssuedAt', 'iat', undefined, undefined, now) -test(testJWTsetFunction, 'setExpirationTime', 'exp', 0) -test(testJWTsetFunction, 'setExpirationTime', 'exp', '10s', undefined, now + 10) -test(testJWTsetFunction, 'setNotBefore', 'nbf', 0) -test(testJWTsetFunction, 'setNotBefore', 'nbf', '10s', undefined, now + 10) test('EncryptJWT.prototype.setProtectedHeader', (t) => { t.throws(() => new EncryptJWT(t.context.payload).setProtectedHeader({}).setProtectedHeader({}), { diff --git a/test/jwt/sign.test.mjs b/test/jwt/sign.test.mjs index 2e7c472667..95a8daf1cd 100644 --- a/test/jwt/sign.test.mjs +++ b/test/jwt/sign.test.mjs @@ -1,5 +1,6 @@ import test from 'ava' import timekeeper from 'timekeeper' +import { setters } from './time_setters.mjs' const { SignJWT, compactVerify, jwtVerify } = await import('#dist') @@ -99,18 +100,10 @@ async function testJWTsetFunction(t, method, claim, value, expected = value) { t.is(claims[claim], expected) } testJWTsetFunction.title = (title, method, claim, value) => - `SignJWT.prototype.${method} called with ${value?.constructor?.name || typeof value}` + `SignJWT.prototype.${method} called with ${value?.constructor?.name || typeof value} (${value})` -test(testJWTsetFunction, 'setIssuer', 'iss', 'urn:example:issuer') -test(testJWTsetFunction, 'setSubject', 'sub', 'urn:example:subject') -test(testJWTsetFunction, 'setAudience', 'aud', 'urn:example:audience') -test(testJWTsetFunction, 'setJti', 'jti', 'urn:example:jti') -test(testJWTsetFunction, 'setIssuedAt', 'iat', 0) -test(testJWTsetFunction, 'setIssuedAt', 'iat', undefined, now) -test(testJWTsetFunction, 'setIssuedAt', 'iat', new Date(now * 1000), now) -test(testJWTsetFunction, 'setExpirationTime', 'exp', 0) -test(testJWTsetFunction, 'setExpirationTime', 'exp', '10s', now + 10) -test(testJWTsetFunction, 'setExpirationTime', 'exp', new Date(now * 1000), now) -test(testJWTsetFunction, 'setNotBefore', 'nbf', 0) -test(testJWTsetFunction, 'setNotBefore', 'nbf', '10s', now + 10) -test(testJWTsetFunction, 'setNotBefore', 'nbf', new Date(now * 1000), now) +for (const [method, claim, vectors] of setters(now)) { + for (const [input, output = input] of vectors) { + test(testJWTsetFunction, method, claim, input, output) + } +} diff --git a/test/jwt/time_setters.mjs b/test/jwt/time_setters.mjs new file mode 100644 index 0000000000..1f80021870 --- /dev/null +++ b/test/jwt/time_setters.mjs @@ -0,0 +1,23 @@ +export function setters(now) { + const timeSetters = [ + [0], + [new Date(now * 1000), now], + ['10s', now + 10], + ['+10s', now + 10], + ['-10s', now - 10], + ['+ 10s', now + 10], + ['- 10s', now - 10], + ['10s from now', now + 10], + ['10s ago', now - 10], + ] + + return [ + ['setIssuer', 'iss', [['urn:example:issuer']]], + ['setSubject', 'sub', [['urn:example:subject']]], + ['setAudience', 'aud', [['urn:example:audience']]], + ['setJti', 'jti', [['urn:example:jti']]], + ['setIssuedAt', 'iat', [[undefined, now], ...timeSetters]], + ['setExpirationTime', 'exp', timeSetters], + ['setNotBefore', 'nbf', timeSetters], + ] +} diff --git a/test/jwt/unsecured.test.mjs b/test/jwt/unsecured.test.mjs index 9253d779b4..70ac4c90d4 100644 --- a/test/jwt/unsecured.test.mjs +++ b/test/jwt/unsecured.test.mjs @@ -1,7 +1,8 @@ import test from 'ava' import timekeeper from 'timekeeper' +import { setters } from './time_setters.mjs' -const { UnsecuredJWT } = await import('#dist') +const { UnsecuredJWT, decodeJwt } = await import('#dist') const now = 1604416038 @@ -47,18 +48,17 @@ test('new UnsecuredJWT()', (t) => { async function testJWTsetFunction(t, method, claim, value, expected = value) { const jwt = new UnsecuredJWT()[method](value).encode() - const { payload: claims } = UnsecuredJWT.decode(jwt) + const claims = decodeJwt(jwt) t.true(claim in claims) t.is(claims[claim], expected) } testJWTsetFunction.title = (title, method, claim, value) => - `UnsecuredJWT.prototype.${method} called with ${value}` + `UnsecuredJWT.prototype.${method} called with ${ + value?.constructor?.name || typeof value + } (${value})` -test(testJWTsetFunction, 'setIssuer', 'iss', 'urn:example:issuer') -test(testJWTsetFunction, 'setSubject', 'sub', 'urn:example:subject') -test(testJWTsetFunction, 'setAudience', 'aud', 'urn:example:audience') -test(testJWTsetFunction, 'setJti', 'jti', 'urn:example:jti') -test(testJWTsetFunction, 'setIssuedAt', 'iat', 0) -test(testJWTsetFunction, 'setIssuedAt', 'iat', undefined, now) -test(testJWTsetFunction, 'setExpirationTime', 'exp', '10s', now + 10) -test(testJWTsetFunction, 'setNotBefore', 'nbf', 0) +for (const [method, claim, vectors] of setters(now)) { + for (const [input, output = input] of vectors) { + test(testJWTsetFunction, method, claim, input, output) + } +} diff --git a/test/unit/secs.test.mjs b/test/unit/secs.test.mjs index eb2a2ce45f..229a060127 100644 --- a/test/unit/secs.test.mjs +++ b/test/unit/secs.test.mjs @@ -3,33 +3,109 @@ import test from 'ava' const { default: secs } = await import('#dist/lib/secs') test('lib/secs.ts', (t) => { + for (const sign of ['+', '+ ', '']) { + for (const v of ['sec', 'secs', 'second', 'seconds', 's']) { + t.is(secs(`${sign}1${v}`), 1) + t.is(secs(`${sign}1 ${v}`), 1) + } + for (const v of ['minute', 'minutes', 'min', 'mins', 'm']) { + t.is(secs(`${sign}1${v}`), 60) + t.is(secs(`${sign}1 ${v}`), 60) + } + for (const v of ['hour', 'hours', 'hr', 'hrs', 'h']) { + t.is(secs(`${sign}1${v}`), 3600) + t.is(secs(`${sign}1 ${v}`), 3600) + } + for (const v of ['day', 'days', 'd']) { + t.is(secs(`${sign}1${v}`), 86400) + t.is(secs(`${sign}1 ${v}`), 86400) + } + for (const v of ['week', 'weeks', 'w']) { + t.is(secs(`${sign}1${v}`), 604800) + t.is(secs(`${sign}1 ${v}`), 604800) + } + for (const v of ['years', 'year', 'yrs', 'yr', 'y']) { + t.is(secs(`${sign}1${v}`), 31557600) + t.is(secs(`${sign}1 ${v}`), 31557600) + } + } + + for (const sign of ['-', '- ']) { + for (const v of ['sec', 'secs', 'second', 'seconds', 's']) { + t.is(secs(`${sign}1${v}`), -1) + t.is(secs(`${sign}1 ${v}`), -1) + } + for (const v of ['minute', 'minutes', 'min', 'mins', 'm']) { + t.is(secs(`${sign}1${v}`), -60) + t.is(secs(`${sign}1 ${v}`), -60) + } + for (const v of ['hour', 'hours', 'hr', 'hrs', 'h']) { + t.is(secs(`${sign}1${v}`), -3600) + t.is(secs(`${sign}1 ${v}`), -3600) + } + for (const v of ['day', 'days', 'd']) { + t.is(secs(`${sign}1${v}`), -86400) + t.is(secs(`${sign}1 ${v}`), -86400) + } + for (const v of ['week', 'weeks', 'w']) { + t.is(secs(`${sign}1${v}`), -604800) + t.is(secs(`${sign}1 ${v}`), -604800) + } + for (const v of ['years', 'year', 'yrs', 'yr', 'y']) { + t.is(secs(`${sign}1${v}`), -31557600) + t.is(secs(`${sign}1 ${v}`), -31557600) + } + } + for (const v of ['sec', 'secs', 'second', 'seconds', 's']) { - t.is(secs(`1${v}`), 1) - t.is(secs(`1 ${v}`), 1) + t.is(secs(`1${v} ago`), -1) + t.is(secs(`1${v} from now`), 1) + t.is(secs(`1 ${v} ago`), -1) + t.is(secs(`1 ${v} from now`), 1) } for (const v of ['minute', 'minutes', 'min', 'mins', 'm']) { - t.is(secs(`1${v}`), 60) - t.is(secs(`1 ${v}`), 60) + t.is(secs(`1${v} ago`), -60) + t.is(secs(`1${v} from now`), 60) + t.is(secs(`1 ${v} ago`), -60) + t.is(secs(`1 ${v} from now`), 60) } for (const v of ['hour', 'hours', 'hr', 'hrs', 'h']) { - t.is(secs(`1${v}`), 3600) - t.is(secs(`1 ${v}`), 3600) + t.is(secs(`1${v} ago`), -3600) + t.is(secs(`1${v} from now`), 3600) + t.is(secs(`1 ${v} ago`), -3600) + t.is(secs(`1 ${v} from now`), 3600) } for (const v of ['day', 'days', 'd']) { - t.is(secs(`1${v}`), 86400) - t.is(secs(`1 ${v}`), 86400) + t.is(secs(`1${v} ago`), -86400) + t.is(secs(`1${v} from now`), 86400) + t.is(secs(`1 ${v} ago`), -86400) + t.is(secs(`1 ${v} from now`), 86400) } for (const v of ['week', 'weeks', 'w']) { - t.is(secs(`1${v}`), 604800) - t.is(secs(`1 ${v}`), 604800) + t.is(secs(`1${v} ago`), -604800) + t.is(secs(`1${v} from now`), 604800) + t.is(secs(`1 ${v} ago`), -604800) + t.is(secs(`1 ${v} from now`), 604800) } for (const v of ['years', 'year', 'yrs', 'yr', 'y']) { - t.is(secs(`1${v}`), 31557600) - t.is(secs(`1 ${v}`), 31557600) + t.is(secs(`1${v} ago`), -31557600) + t.is(secs(`1${v} from now`), 31557600) + t.is(secs(`1 ${v} ago`), -31557600) + t.is(secs(`1 ${v} from now`), 31557600) } t.throws(() => secs('1 fortnight'), { instanceOf: TypeError, message: 'Invalid time period format', }) + + t.throws(() => secs('= 1 second'), { + instanceOf: TypeError, + message: 'Invalid time period format', + }) + + t.throws(() => secs('- 1 second ago'), { + instanceOf: TypeError, + message: 'Invalid time period format', + }) })