From 765afcccb1674b409e106eb8f80918fd09cf0877 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 8 Jul 2021 14:25:03 -0400 Subject: [PATCH 1/4] Add custom ttl options for App Check --- etc/firebase-admin.api.md | 5 +- src/app-check/app-check.ts | 10 ++- src/app-check/index.ts | 17 +++- src/app-check/token-generator.ts | 46 ++++++++++- src/messaging/messaging-internal.ts | 27 +----- src/utils/index.ts | 26 ++++++ test/unit/app-check/app-check.spec.ts | 9 ++ test/unit/app-check/token-generator.spec.ts | 91 ++++++++++++++++++++- test/unit/utils/index.spec.ts | 15 +++- 9 files changed, 206 insertions(+), 40 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 6017b0c72a..427bc43099 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -53,13 +53,16 @@ export namespace appCheck { export interface AppCheck { // (undocumented) app: app.App; - createToken(appId: string): Promise; + createToken(appId: string, options?: AppCheckTokenOptions): Promise; verifyToken(appCheckToken: string): Promise; } export interface AppCheckToken { token: string; ttlMillis: number; } + export interface AppCheckTokenOptions { + ttlMillis?: number; + } export interface DecodedAppCheckToken { // (undocumented) [key: string]: any; diff --git a/src/app-check/app-check.ts b/src/app-check/app-check.ts index 42d8391043..175d023419 100644 --- a/src/app-check/app-check.ts +++ b/src/app-check/app-check.ts @@ -26,6 +26,7 @@ import { cryptoSignerFromApp } from '../utils/crypto-signer'; import AppCheckInterface = appCheck.AppCheck; import AppCheckToken = appCheck.AppCheckToken; +import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; import VerifyAppCheckTokenResponse = appCheck.VerifyAppCheckTokenResponse; /** @@ -56,18 +57,19 @@ export class AppCheck implements AppCheckInterface { * back to a client. * * @param appId The app ID to use as the JWT app_id. + * @param options Optional options object when creating a new App Check Token. * - * @return A promise that fulfills with a `AppCheckToken`. + * @returns A promise that fulfills with a `AppCheckToken`. */ - public createToken(appId: string): Promise { - return this.tokenGenerator.createCustomToken(appId) + public createToken(appId: string, options?: AppCheckTokenOptions): Promise { + return this.tokenGenerator.createCustomToken(appId, options) .then((customToken) => { return this.client.exchangeToken(customToken, appId); }); } /** - * Veifies an App Check token. + * Verifies an App Check token. * * @param appCheckToken The App Check token to verify. * diff --git a/src/app-check/index.ts b/src/app-check/index.ts index 6552d9208d..6819c72647 100644 --- a/src/app-check/index.ts +++ b/src/app-check/index.ts @@ -61,10 +61,11 @@ export namespace appCheck { * back to a client. * * @param appId The App ID of the Firebase App the token belongs to. + * @param options Optional options object when creating a new App Check Token. * - * @return A promise that fulfills with a `AppCheckToken`. + * @returns A promise that fulfills with a `AppCheckToken`. */ - createToken(appId: string): Promise; + createToken(appId: string, options?: AppCheckTokenOptions): Promise; /** * Verifies a Firebase App Check token (JWT). If the token is valid, the promise is @@ -95,6 +96,18 @@ export namespace appCheck { ttlMillis: number; } + /** + * Interface representing an App Check token options. + */ + export interface AppCheckTokenOptions { + /** + * The length of time measured in milliseconds starting from when the server + * mints the token for which the returned FAC token will be valid. + * This value must be in milliseconds and between 30 minutes and 7 days, inclusive. + */ + ttlMillis?: number; + } + /** * Interface representing a decoded Firebase App Check token, returned from the * {@link appCheck.AppCheck.verifyToken `verifyToken()`} method. diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index 1b557438bb..8f02ed0769 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -15,8 +15,10 @@ * limitations under the License. */ +import { appCheck } from './index'; + import * as validator from '../utils/validator'; -import { toWebSafeBase64 } from '../utils'; +import { toWebSafeBase64, transformMillisecondsToSecondsString } from '../utils'; import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; import { @@ -26,7 +28,11 @@ import { } from './app-check-api-client-internal'; import { HttpError } from '../utils/api-request'; +import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; + const ONE_HOUR_IN_SECONDS = 60 * 60; +const ONE_MINUTE_IN_MILLIS = 60 * 1000; +const ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; // Audience to use for Firebase App Check Custom tokens const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; @@ -63,12 +69,16 @@ export class AppCheckTokenGenerator { * @return A Promise fulfilled with a custom token signed with a service account key * that can be exchanged to an App Check token. */ - public createCustomToken(appId: string): Promise { + public createCustomToken(appId: string, options?: AppCheckTokenOptions): Promise { if (!validator.isNonEmptyString(appId)) { throw new FirebaseAppCheckError( 'invalid-argument', '`appId` must be a non-empty string.'); } + let customOptions = {}; + if (typeof options !== 'undefined') { + customOptions = this.validateTokenOptions(options); + } return this.signer.getAccountId().then((account) => { const header = { alg: this.signer.algorithm, @@ -83,6 +93,7 @@ export class AppCheckTokenGenerator { aud: FIREBASE_APP_CHECK_AUDIENCE, exp: iat + ONE_HOUR_IN_SECONDS, iat, + ...customOptions, }; const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`; return this.signer.sign(Buffer.from(token)) @@ -98,6 +109,35 @@ export class AppCheckTokenGenerator { const buffer: Buffer = (segment instanceof Buffer) ? segment : Buffer.from(JSON.stringify(segment)); return toWebSafeBase64(buffer).replace(/=+$/, ''); } + + /** + * Checks if a given `AppCheckTokenOptions` object is valid. If successful, returns an object with + * custom properties. + * + * @param options An options object to be validated. + * @returns A custom object with ttl converted to protobuf Duration string format. + */ + private validateTokenOptions(options: AppCheckTokenOptions): {[key: string]: any} { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'AppCheckTokenOptions must be a non-null object.'); + } + if (typeof options.ttlMillis !== 'undefined') { + if (!validator.isNumber(options.ttlMillis) || options.ttlMillis < 0) { + throw new FirebaseAppCheckError('invalid-argument', + 'ttlMillis must be a non-negative duration in milliseconds.'); + } + // ttlMillis must be between 30 minutes and 7 days (inclusive) + if (options.ttlMillis < (ONE_MINUTE_IN_MILLIS * 30) || options.ttlMillis > (ONE_DAY_IN_MILLIS * 7)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).'); + } + return { ttl: transformMillisecondsToSecondsString(options.ttlMillis) }; + } + return {}; + } } /** @@ -123,7 +163,7 @@ export function appCheckErrorFromCryptoSignerError(err: Error): Error { code = APP_CHECK_ERROR_CODE_MAPPING[status]; } return new FirebaseAppCheckError(code, - `Error returned from server while siging a custom token: ${description}` + `Error returned from server while signing a custom token: ${description}` ); } return new FirebaseAppCheckError('internal-error', diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts index 227f894eea..d84328e722 100644 --- a/src/messaging/messaging-internal.ts +++ b/src/messaging/messaging-internal.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renameProperties } from '../utils/index'; +import { renameProperties, transformMillisecondsToSecondsString } from '../utils/index'; import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; import { messaging } from './index'; import * as validator from '../utils/validator'; @@ -589,28 +589,3 @@ function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined): v MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } - -/** - * Transforms milliseconds to the format expected by FCM service. - * Returns the duration in seconds with up to nine fractional - * digits, terminated by 's'. Example: "3.5s". - * - * @param {number} milliseconds The duration in milliseconds. - * @return {string} The resulting formatted string in seconds with up to nine fractional - * digits, terminated by 's'. - */ -function transformMillisecondsToSecondsString(milliseconds: number): string { - let duration: string; - const seconds = Math.floor(milliseconds / 1000); - const nanos = (milliseconds - seconds * 1000) * 1000000; - if (nanos > 0) { - let nanoString = nanos.toString(); - while (nanoString.length < 9) { - nanoString = '0' + nanoString; - } - duration = `${seconds}.${nanoString}s`; - } else { - duration = `${seconds}s`; - } - return duration; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index b8cfa2faf0..eb38001bb5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -190,3 +190,29 @@ export function generateUpdateMask( } return updateMask; } + +/** + * Transforms milliseconds to a protobuf Duration type string. + * Returns the duration in seconds with up to nine fractional + * digits, terminated by 's'. Example: "3 seconds 0 nano seconds as 3s, + * 3 seconds 1 nano seconds as 3.000000001s". + * + * @param milliseconds The duration in milliseconds. + * @returns The resulting formatted string in seconds with up to nine fractional + * digits, terminated by 's'. + */ +export function transformMillisecondsToSecondsString(milliseconds: number): string { + let duration: string; + const seconds = Math.floor(milliseconds / 1000); + const nanos = Math.round((milliseconds - seconds * 1000) * 1000000); + if (nanos > 0) { + let nanoString = nanos.toString(); + while (nanoString.length < 9) { + nanoString = '0' + nanoString; + } + duration = `${seconds}.${nanoString}s`; + } else { + duration = `${seconds}s`; + } + return duration; +} diff --git a/test/unit/app-check/app-check.spec.ts b/test/unit/app-check/app-check.spec.ts index c30970bc13..05c598f301 100644 --- a/test/unit/app-check/app-check.spec.ts +++ b/test/unit/app-check/app-check.spec.ts @@ -147,6 +147,15 @@ describe('AppCheck', () => { .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); }); + it('should propagate API errors with custom options', () => { + const stub = sinon + .stub(AppCheckApiClient.prototype, 'exchangeToken') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return appCheck.createToken(APP_ID, { ttlMillis: 1800000 }) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + it('should resolve with AppCheckToken on success', () => { const response = { token: 'token', ttlMillis: 3000 }; const stub = sinon diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index 66bb7cffba..08d88e3f82 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -122,11 +122,49 @@ describe('AppCheckTokenGenerator', () => { }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); }); - it('should be fulfilled with a Firebase Custom JWT', () => { + const invalidOptions = [null, NaN, 0, 1, true, false, [], _.noop]; + invalidOptions.forEach((invalidOption) => { + it('should throw given an invalid options: ' + JSON.stringify(invalidOption), () => { + expect(() => { + tokenGenerator.createCustomToken(APP_ID, invalidOption as any); + }).to.throw(FirebaseAppCheckError).with.property('message', 'AppCheckTokenOptions must be a non-null object.'); + }); + }); + + const invalidTtls = [null, NaN, '0', 'abc', '', -100, -1, true, false, [], {}, { a: 1 }, _.noop]; + invalidTtls.forEach((invalidTtl) => { + it('should throw given an options object with invalid ttl: ' + JSON.stringify(invalidTtl), () => { + expect(() => { + tokenGenerator.createCustomToken(APP_ID, { ttlMillis: invalidTtl as any }); + }).to.throw(FirebaseAppCheckError).with.property('message', + 'ttlMillis must be a non-negative duration in milliseconds.'); + }); + }); + + const THIRTY_MIN_IN_MS = 1800000; + const SEVEN_DAYS_IN_MS = 604800000; + [0, 10, THIRTY_MIN_IN_MS - 1, SEVEN_DAYS_IN_MS + 1, SEVEN_DAYS_IN_MS * 2].forEach((ttlMillis) => { + it('should throw given options with ttl < 30 minutes or ttl > 7 days:' + JSON.stringify(ttlMillis), () => { + expect(() => { + tokenGenerator.createCustomToken(APP_ID, { ttlMillis }); + }).to.throw(FirebaseAppCheckError).with.property( + 'message', 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).'); + }); + }); + + it('should be fulfilled with a Firebase Custom JWT with only an APP ID', () => { return tokenGenerator.createCustomToken(APP_ID) .should.eventually.be.a('string').and.not.be.empty; }); + [THIRTY_MIN_IN_MS, THIRTY_MIN_IN_MS + 1, SEVEN_DAYS_IN_MS / 2, SEVEN_DAYS_IN_MS - 1, SEVEN_DAYS_IN_MS] + .forEach((ttlMillis) => { + it('should be fulfilled with a Firebase Custom JWT with a valid custom ttl' + JSON.stringify(ttlMillis), () => { + return tokenGenerator.createCustomToken(APP_ID, { ttlMillis }) + .should.eventually.be.a('string').and.not.be.empty; + }); + }); + it('should be fulfilled with a JWT with the correct decoded payload', () => { clock = sinon.useFakeTimers(1000); @@ -147,6 +185,53 @@ describe('AppCheckTokenGenerator', () => { }); }); + [{}, { ttlMillis: undefined }, { a: 123 }].forEach((options) => { + it('should be fulfilled with no ttl in the decoded payload when ttl is not provided in options', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(APP_ID, options) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: APP_ID, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_APP_CHECK_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + }; + + expect(decoded).to.deep.equal(expected); + }); + }); + }); + + [[1800000.000001, '1800.000000001s'], [1800000.001, '1800.000001000s'], [172800000, '172800s'], + [604799999, '604799.999000000s'], [604800000, '604800s'] + ].forEach((ttl) => { + it('should be fulfilled with a JWT with custom ttl in decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(APP_ID, { ttlMillis: ttl[0] as number }) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: APP_ID, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_APP_CHECK_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + ttl: ttl[1], + }; + + expect(decoded).to.deep.equal(expected); + }); + }); + }); + it('should be fulfilled with a JWT with the correct header', () => { clock = sinon.useFakeTimers(1000); @@ -225,7 +310,7 @@ describe('AppCheckTokenGenerator', () => { expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); expect(appCheckError).to.have.property('message', - 'Error returned from server while siging a custom token: server error.'); + 'Error returned from server while signing a custom token: server error.'); }); it('should convert CryptoSignerError HttpError with no error.message to FirebaseAppCheckError', () => { @@ -240,7 +325,7 @@ describe('AppCheckTokenGenerator', () => { expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); expect(appCheckError).to.have.property('message', - 'Error returned from server while siging a custom token: '+ + 'Error returned from server while signing a custom token: '+ '{"status":500,"headers":{},"data":{"error":{}},"text":"{\\"error\\":{}}"}'); }); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index e63993fb83..a31b2c8d2f 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -22,7 +22,7 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { addReadonlyGetter, getExplicitProjectId, findProjectId, - toWebSafeBase64, formatString, generateUpdateMask, + toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp } from '../../../src/firebase-app'; @@ -383,3 +383,16 @@ describe('generateUpdateMask()', () => { .to.deep.equal(['b', 'c', 'd', 'e', 'f', 'k', 'l', 'n']); }); }); + + +describe('transformMillisecondsToSecondsString()', () => { + [ + [3000.000001, '3.000000001s'], [3000.001, '3.000001000s'], + [3000, '3s'], [3500, '3.500000000s'] + ].forEach((duration) => { + it('should transform to protobuf duration string when provided milliseconds:' + JSON.stringify(duration[0]), + () => { + expect(transformMillisecondsToSecondsString(duration[0] as number)).to.equal(duration[1]); + }); + }); +}); From 9e1154e7a56b8eddc20aedb3842c0c80741e3c8f Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 9 Jul 2021 17:02:18 -0400 Subject: [PATCH 2/4] PR fixes --- src/app-check/index.ts | 2 +- src/app-check/token-generator.ts | 4 ++-- src/utils/index.ts | 2 +- test/unit/app-check/token-generator.spec.ts | 23 +++++++++++++-------- test/unit/utils/index.spec.ts | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app-check/index.ts b/src/app-check/index.ts index 6819c72647..867a68e3fb 100644 --- a/src/app-check/index.ts +++ b/src/app-check/index.ts @@ -97,7 +97,7 @@ export namespace appCheck { } /** - * Interface representing an App Check token options. + * Interface representing App Check token options. */ export interface AppCheckTokenOptions { /** diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index 8f02ed0769..6c5b9eeda6 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -124,9 +124,9 @@ export class AppCheckTokenGenerator { 'AppCheckTokenOptions must be a non-null object.'); } if (typeof options.ttlMillis !== 'undefined') { - if (!validator.isNumber(options.ttlMillis) || options.ttlMillis < 0) { + if (!validator.isNumber(options.ttlMillis)) { throw new FirebaseAppCheckError('invalid-argument', - 'ttlMillis must be a non-negative duration in milliseconds.'); + 'ttlMillis must be a duration in milliseconds.'); } // ttlMillis must be between 30 minutes and 7 days (inclusive) if (options.ttlMillis < (ONE_MINUTE_IN_MILLIS * 30) || options.ttlMillis > (ONE_DAY_IN_MILLIS * 7)) { diff --git a/src/utils/index.ts b/src/utils/index.ts index eb38001bb5..d047397667 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -204,7 +204,7 @@ export function generateUpdateMask( export function transformMillisecondsToSecondsString(milliseconds: number): string { let duration: string; const seconds = Math.floor(milliseconds / 1000); - const nanos = Math.round((milliseconds - seconds * 1000) * 1000000); + const nanos = Math.floor((milliseconds - seconds * 1000) * 1000000); if (nanos > 0) { let nanoString = nanos.toString(); while (nanoString.length < 9) { diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index 08d88e3f82..c29575f21a 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -131,19 +131,19 @@ describe('AppCheckTokenGenerator', () => { }); }); - const invalidTtls = [null, NaN, '0', 'abc', '', -100, -1, true, false, [], {}, { a: 1 }, _.noop]; + const invalidTtls = [null, NaN, '0', 'abc', '', true, false, [], {}, { a: 1 }, _.noop]; invalidTtls.forEach((invalidTtl) => { it('should throw given an options object with invalid ttl: ' + JSON.stringify(invalidTtl), () => { expect(() => { tokenGenerator.createCustomToken(APP_ID, { ttlMillis: invalidTtl as any }); }).to.throw(FirebaseAppCheckError).with.property('message', - 'ttlMillis must be a non-negative duration in milliseconds.'); + 'ttlMillis must be a duration in milliseconds.'); }); }); const THIRTY_MIN_IN_MS = 1800000; const SEVEN_DAYS_IN_MS = 604800000; - [0, 10, THIRTY_MIN_IN_MS - 1, SEVEN_DAYS_IN_MS + 1, SEVEN_DAYS_IN_MS * 2].forEach((ttlMillis) => { + [-100, -1, 0, 10, THIRTY_MIN_IN_MS - 1, SEVEN_DAYS_IN_MS + 1, SEVEN_DAYS_IN_MS * 2].forEach((ttlMillis) => { it('should throw given options with ttl < 30 minutes or ttl > 7 days:' + JSON.stringify(ttlMillis), () => { expect(() => { tokenGenerator.createCustomToken(APP_ID, { ttlMillis }); @@ -157,11 +157,16 @@ describe('AppCheckTokenGenerator', () => { .should.eventually.be.a('string').and.not.be.empty; }); - [THIRTY_MIN_IN_MS, THIRTY_MIN_IN_MS + 1, SEVEN_DAYS_IN_MS / 2, SEVEN_DAYS_IN_MS - 1, SEVEN_DAYS_IN_MS] - .forEach((ttlMillis) => { - it('should be fulfilled with a Firebase Custom JWT with a valid custom ttl' + JSON.stringify(ttlMillis), () => { - return tokenGenerator.createCustomToken(APP_ID, { ttlMillis }) - .should.eventually.be.a('string').and.not.be.empty; + [[THIRTY_MIN_IN_MS, '1800s'], [THIRTY_MIN_IN_MS + 1, '1800.001000000s'], + [SEVEN_DAYS_IN_MS / 2, '302400s'], [SEVEN_DAYS_IN_MS - 1, '604799.999000000s'], [SEVEN_DAYS_IN_MS, '604800s']] + .forEach((ttl) => { + it('should be fulfilled with a Firebase Custom JWT with a valid custom ttl' + JSON.stringify(ttl[0]), () => { + return tokenGenerator.createCustomToken(APP_ID, { ttlMillis: ttl[0] as number }) + .then((token) => { + const decoded = jwt.decode(token) as { [key: string]: any }; + + expect(decoded['ttl']).to.equal(ttl[1]); + }); }); }); @@ -207,7 +212,7 @@ describe('AppCheckTokenGenerator', () => { }); }); - [[1800000.000001, '1800.000000001s'], [1800000.001, '1800.000001000s'], [172800000, '172800s'], + [[1800000.000001, '1800.000000001s'], [1800000.001, '1800.000000999s'], [172800000, '172800s'], [604799999, '604799.999000000s'], [604800000, '604800s'] ].forEach((ttl) => { it('should be fulfilled with a JWT with custom ttl in decoded payload', () => { diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index a31b2c8d2f..9729dc7817 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -387,7 +387,7 @@ describe('generateUpdateMask()', () => { describe('transformMillisecondsToSecondsString()', () => { [ - [3000.000001, '3.000000001s'], [3000.001, '3.000001000s'], + [3000.000001, '3s'], [3000.001, '3.000001000s'], [3000, '3s'], [3500, '3.500000000s'] ].forEach((duration) => { it('should transform to protobuf duration string when provided milliseconds:' + JSON.stringify(duration[0]), From 53226ab9777fbae56a1d963d2c4e6f59169799af Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 9 Jul 2021 17:24:21 -0400 Subject: [PATCH 3/4] Add integration tests --- test/integration/app-check.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/integration/app-check.spec.ts b/test/integration/app-check.spec.ts index 32386f32bc..82ad153498 100644 --- a/test/integration/app-check.spec.ts +++ b/test/integration/app-check.spec.ts @@ -53,6 +53,20 @@ describe('admin.appCheck', () => { expect(token).to.have.keys(['token', 'ttlMillis']); expect(token.token).to.be.a('string').and.to.not.be.empty; expect(token.ttlMillis).to.be.a('number'); + expect(token.ttlMillis).to.equals(3600000); + }); + }); + + it('should succeed with a valid token and a custom ttl', function() { + if (!appId) { + this.skip(); + } + return admin.appCheck().createToken(appId as string, { ttlMillis: 1800000 }) + .then((token) => { + expect(token).to.have.keys(['token', 'ttlMillis']); + expect(token.token).to.be.a('string').and.to.not.be.empty; + expect(token.ttlMillis).to.be.a('number'); + expect(token.ttlMillis).to.equals(1800000); }); }); From 752437da5fca49ee22733bd99fe94ab8bc817ad2 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 12 Jul 2021 16:07:17 -0400 Subject: [PATCH 4/4] PR fixes --- src/app-check/index.ts | 5 ++- test/unit/app-check/token-generator.spec.ts | 34 +++++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/app-check/index.ts b/src/app-check/index.ts index 867a68e3fb..295f49e4be 100644 --- a/src/app-check/index.ts +++ b/src/app-check/index.ts @@ -101,9 +101,8 @@ export namespace appCheck { */ export interface AppCheckTokenOptions { /** - * The length of time measured in milliseconds starting from when the server - * mints the token for which the returned FAC token will be valid. - * This value must be in milliseconds and between 30 minutes and 7 days, inclusive. + * The length of time, in milliseconds, for which the App Check token will + * be valid. This value must be between 30 minutes and 7 days, inclusive. */ ttlMillis?: number; } diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index c29575f21a..2cf10b8bc5 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -157,18 +157,22 @@ describe('AppCheckTokenGenerator', () => { .should.eventually.be.a('string').and.not.be.empty; }); - [[THIRTY_MIN_IN_MS, '1800s'], [THIRTY_MIN_IN_MS + 1, '1800.001000000s'], - [SEVEN_DAYS_IN_MS / 2, '302400s'], [SEVEN_DAYS_IN_MS - 1, '604799.999000000s'], [SEVEN_DAYS_IN_MS, '604800s']] - .forEach((ttl) => { - it('should be fulfilled with a Firebase Custom JWT with a valid custom ttl' + JSON.stringify(ttl[0]), () => { - return tokenGenerator.createCustomToken(APP_ID, { ttlMillis: ttl[0] as number }) - .then((token) => { - const decoded = jwt.decode(token) as { [key: string]: any }; - - expect(decoded['ttl']).to.equal(ttl[1]); - }); - }); + [ + [THIRTY_MIN_IN_MS, '1800s'], + [THIRTY_MIN_IN_MS + 1, '1800.001000000s'], + [SEVEN_DAYS_IN_MS / 2, '302400s'], + [SEVEN_DAYS_IN_MS - 1, '604799.999000000s'], + [SEVEN_DAYS_IN_MS, '604800s'] + ].forEach((ttl) => { + it('should be fulfilled with a Firebase Custom JWT with a valid custom ttl' + JSON.stringify(ttl[0]), () => { + return tokenGenerator.createCustomToken(APP_ID, { ttlMillis: ttl[0] as number }) + .then((token) => { + const decoded = jwt.decode(token) as { [key: string]: any }; + + expect(decoded['ttl']).to.equal(ttl[1]); + }); }); + }); it('should be fulfilled with a JWT with the correct decoded payload', () => { clock = sinon.useFakeTimers(1000); @@ -212,8 +216,12 @@ describe('AppCheckTokenGenerator', () => { }); }); - [[1800000.000001, '1800.000000001s'], [1800000.001, '1800.000000999s'], [172800000, '172800s'], - [604799999, '604799.999000000s'], [604800000, '604800s'] + [ + [1800000.000001, '1800.000000001s'], + [1800000.001, '1800.000000999s'], + [172800000, '172800s'], + [604799999, '604799.999000000s'], + [604800000, '604800s'] ].forEach((ttl) => { it('should be fulfilled with a JWT with custom ttl in decoded payload', () => { clock = sinon.useFakeTimers(1000);