From 1ab860dcb25a467d2f08f308815f422c72c1d1ed Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Thu, 7 Apr 2022 09:13:27 -0700 Subject: [PATCH 1/5] adding in alpha interface --- src/auth/base-auth.ts | 15 +++++ src/auth/token-verifier.ts | 122 +++++++++++++++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 5 deletions(-) diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index e1a8330ff7..b7ec30ee26 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -24,6 +24,7 @@ import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, DecodedIdToken, + DecodedAuthBlockingToken, } from './token-verifier'; import { AuthProviderConfig, SAMLAuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, @@ -1055,6 +1056,20 @@ export abstract class BaseAuth { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } + /* eslint-disable */ + /** @alpha */ + public _verifyAuthBlockingToken( + token: string, + audience?: string + ): Promise { + const isEmulator = useEmulator(); + return this.idTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience) + .then((decodedIdToken: DecodedAuthBlockingToken) => { + return decodedIdToken; + }); + } + /* eslint-enable */ + /** * Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that * resolves with the decoded claims on success. Rejects the promise with revocation error if revoked diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index d38cb7bd96..5224c4d21e 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -176,6 +176,82 @@ export interface DecodedIdToken { [key: string]: any; } +/** @alpha */ +interface DecodedAuthBlockingSharedUserInfo { + uid: string; + display_name?: string; + email?: string; + photo_url?: string; + phone_number?: string; +} + +/** @alpha */ +interface DecodedAuthBlockingMetadata { + creation_time?: number; + last_sign_in_time?: number; +} + +/** @alpha */ +interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo { + provider_id: string; +} + +/** @alpha */ +interface DecodedAuthBlockingMfaInfo { + uid: string; + display_name?: string; + phone_number?: string; + enrollment_time?: string; + factor_id?: string; +} + +/** @alpha */ +interface DecodedAuthBlockingEnrolledFactors { + enrolled_factors?: DecodedAuthBlockingMfaInfo[]; +} + +/** @alpha */ +interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo { + email_verified?: boolean; + disabled?: boolean; + metadata?: DecodedAuthBlockingMetadata; + password_hash?: string; + password_salt?: string; + provider_data?: DecodedAuthBlockingUserInfo[]; + multi_factor?: DecodedAuthBlockingEnrolledFactors; + custom_claims?: any; + tokens_valid_after_time?: number; + tenant_id?: string; + [key: string]: any; +} + +/** @alpha */ +export interface DecodedAuthBlockingToken { + aud: string; + exp: number; + iat: number; + iss: string; + sub: string; + event_id: string; + event_type: string; + ip_address: string; + user_agent?: string; + locale?: string; + sign_in_method?: string; + user_record?: DecodedAuthBlockingUserRecord; + tenant_id?: string; + raw_user_info?: string; + sign_in_attributes?: { + [key: string]: any; + }; + oauth_id_token?: string; + oauth_access_token?: string; + oauth_refresh_token?: string; + oauth_token_secret?: string; + oauth_expires_in?: number; + [key: string]: any; +} + // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; @@ -320,6 +396,34 @@ export class FirebaseTokenVerifier { }); } + /* eslint-disable */ + /** @alpha */ + public _verifyAuthBlockingToken( + jwtToken: string, + isEmulator: boolean, + audience?: string): Promise { + if (!validator.isString(jwtToken)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, + ); + } + + return this.ensureProjectId() + .then((projectId) => { + if (!audience) { + audience = `${projectId}.cloudfunctions.net/`; + } + return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience); + }) + .then((decoded) => { + const decodedIdToken = decoded.payload as DecodedAuthBlockingToken; + decodedIdToken.uid = decodedIdToken.sub; + return decodedIdToken; + }); + } + /* eslint-enable */ + private ensureProjectId(): Promise { return util.findProjectId(this.app) .then((projectId) => { @@ -334,10 +438,14 @@ export class FirebaseTokenVerifier { }) } - private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise { + private decodeAndVerify( + token: string, + projectId: string, + isEmulator: boolean, + audience?: string): Promise { return this.safeDecode(token) .then((decodedToken) => { - this.verifyContent(decodedToken, projectId, isEmulator); + this.verifyContent(decodedToken, projectId, isEmulator, audience); return this.verifySignature(token, isEmulator) .then(() => decodedToken); }); @@ -369,7 +477,8 @@ export class FirebaseTokenVerifier { private verifyContent( fullDecodedToken: DecodedToken, projectId: string | null, - isEmulator: boolean): void { + isEmulator: boolean, + audience: string | undefined): void { const header = fullDecodedToken && fullDecodedToken.header; const payload = fullDecodedToken && fullDecodedToken.payload; @@ -397,9 +506,12 @@ export class FirebaseTokenVerifier { } else if (!isEmulator && header.alg !== ALGORITHM_RS256) { errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; - } else if (payload.aud !== projectId) { + } else if (audience && !(payload.aud as string).includes(audience)) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + + audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage; + } else if (!audience && payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + + projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (payload.iss !== this.issuer + projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + From 18b8504a2bfd1f3e0d55b0e524fc63073db334a8 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Fri, 8 Apr 2022 15:11:34 -0700 Subject: [PATCH 2/5] adding token verifier & unit tests --- etc/firebase-admin.api.md | 2 + etc/firebase-admin.auth.api.md | 52 +++++ src/auth/auth-namespace.ts | 10 +- src/auth/base-auth.ts | 17 +- src/auth/index.ts | 5 +- src/auth/token-verifier.ts | 48 +++- src/utils/error.ts | 4 + test/resources/mocks.ts | 27 +++ test/unit/auth/auth.spec.ts | 128 ++++++++++- test/unit/auth/token-verifier.spec.ts | 314 ++++++++++++++++++++++++++ 10 files changed, 588 insertions(+), 19 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 69b9052166..ad1075cff7 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -106,6 +106,8 @@ export namespace auth { export type CreateRequest = CreateRequest; // Warning: (ae-forgotten-export) The symbol "CreateTenantRequest" needs to be exported by the entry point default-namespace.d.ts export type CreateTenantRequest = CreateTenantRequest; + // Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingToken" needs to be exported by the entry point default-namespace.d.ts + export type DecodedAuthBlockingToken = DecodedAuthBlockingToken; // Warning: (ae-forgotten-export) The symbol "DecodedIdToken" needs to be exported by the entry point default-namespace.d.ts export type DecodedIdToken = DecodedIdToken; // Warning: (ae-forgotten-export) The symbol "DeleteUsersResult" needs to be exported by the entry point default-namespace.d.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 36b2dcf686..16fd6a64e1 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -68,6 +68,8 @@ export abstract class BaseAuth { setCustomUserClaims(uid: string, customUserClaims: object | null): Promise; updateProviderConfig(providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise; updateUser(uid: string, properties: UpdateRequest): Promise; + // @alpha (undocumented) + _verifyAuthBlockingToken(token: string, audience?: string): Promise; verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise; } @@ -110,6 +112,56 @@ export interface CreateRequest extends UpdateRequest { // @public export type CreateTenantRequest = UpdateTenantRequest; +// @alpha (undocumented) +export interface DecodedAuthBlockingToken { + // (undocumented) + [key: string]: any; + // (undocumented) + aud: string; + // (undocumented) + event_id: string; + // (undocumented) + event_type: string; + // (undocumented) + exp: number; + // (undocumented) + iat: number; + // (undocumented) + ip_address: string; + // (undocumented) + iss: string; + // (undocumented) + locale?: string; + // (undocumented) + oauth_access_token?: string; + // (undocumented) + oauth_expires_in?: number; + // (undocumented) + oauth_id_token?: string; + // (undocumented) + oauth_refresh_token?: string; + // (undocumented) + oauth_token_secret?: string; + // (undocumented) + raw_user_info?: string; + // (undocumented) + sign_in_attributes?: { + [key: string]: any; + }; + // (undocumented) + sign_in_method?: string; + // (undocumented) + sub: string; + // (undocumented) + tenant_id?: string; + // (undocumented) + user_agent?: string; + // Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingUserRecord" needs to be exported by the entry point index.d.ts + // + // (undocumented) + user_record?: DecodedAuthBlockingUserRecord; +} + // @public export interface DecodedIdToken { [key: string]: any; diff --git a/src/auth/auth-namespace.ts b/src/auth/auth-namespace.ts index f126ae62f2..75c13532bb 100644 --- a/src/auth/auth-namespace.ts +++ b/src/auth/auth-namespace.ts @@ -73,7 +73,10 @@ import { TenantManager as TTenantManager, } from './tenant-manager'; -import { DecodedIdToken as TDecodedIdToken } from './token-verifier'; +import { + DecodedIdToken as TDecodedIdToken, + DecodedAuthBlockingToken as TDecodedAuthBlockingToken, +} from './token-verifier'; import { HashAlgorithmType as THashAlgorithmType, @@ -173,6 +176,11 @@ export namespace auth { */ export type DecodedIdToken = TDecodedIdToken; + /** + * Type alias to {@link firebase-admin.auth#DecodedAuthBlockingToken}. + */ + export type DecodedAuthBlockingToken = TDecodedAuthBlockingToken; + /** * Type alias to {@link firebase-admin.auth#DeleteUsersResult}. */ diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index b7ec30ee26..7119dd2514 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -22,7 +22,10 @@ import * as validator from '../utils/validator'; import { AbstractAuthRequestHandler, useEmulator } from './auth-api-request'; import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator'; import { - FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, + FirebaseTokenVerifier, + createSessionCookieVerifier, + createIdTokenVerifier, + createAuthBlockingTokenVerifier, DecodedIdToken, DecodedAuthBlockingToken, } from './token-verifier'; @@ -132,6 +135,8 @@ export abstract class BaseAuth { /** @internal */ protected readonly idTokenVerifier: FirebaseTokenVerifier; /** @internal */ + protected readonly authBlockingTokenVerifier: FirebaseTokenVerifier; + /** @internal */ protected readonly sessionCookieVerifier: FirebaseTokenVerifier; /** @@ -157,6 +162,7 @@ export abstract class BaseAuth { this.sessionCookieVerifier = createSessionCookieVerifier(app); this.idTokenVerifier = createIdTokenVerifier(app); + this.authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app); } /** @@ -1056,19 +1062,18 @@ export abstract class BaseAuth { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - /* eslint-disable */ /** @alpha */ + // eslint-disable-next-line @typescript-eslint/naming-convention public _verifyAuthBlockingToken( token: string, audience?: string ): Promise { const isEmulator = useEmulator(); - return this.idTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience) - .then((decodedIdToken: DecodedAuthBlockingToken) => { - return decodedIdToken; + return this.authBlockingTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience) + .then((decodedAuthBlockingToken: DecodedAuthBlockingToken) => { + return decodedAuthBlockingToken; }); } - /* eslint-enable */ /** * Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that diff --git a/src/auth/index.ts b/src/auth/index.ts index 0b92a796cf..5a7e668244 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -116,7 +116,10 @@ export { TenantManager, } from './tenant-manager'; -export { DecodedIdToken } from './token-verifier'; +export { + DecodedIdToken, + DecodedAuthBlockingToken +} from './token-verifier'; export { HashAlgorithmType, diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index 5224c4d21e..bf7581b415 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -277,6 +277,19 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = { expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, }; +/** + * User facing token information related to the Firebase Auth Blocking token. + * + * @internal + */ +export const AUTH_BLOCKING_TOKEN_INFO: FirebaseTokenInfo = { + url: 'https://cloud.google.com/identity-platform/docs/blocking-functions', + verifyApiName: '_verifyAuthBlockingToken()', + jwtName: 'Firebase Auth Blocking token', + shortName: 'Auth Blocking token', + expiredErrorCode: AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED, +}; + /** * User facing token information related to the Firebase session cookie. * @@ -396,12 +409,12 @@ export class FirebaseTokenVerifier { }); } - /* eslint-disable */ /** @alpha */ + // eslint-disable-next-line @typescript-eslint/naming-convention public _verifyAuthBlockingToken( jwtToken: string, isEmulator: boolean, - audience?: string): Promise { + audience: string | undefined): Promise { if (!validator.isString(jwtToken)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -411,18 +424,17 @@ export class FirebaseTokenVerifier { return this.ensureProjectId() .then((projectId) => { - if (!audience) { + if (typeof audience === 'undefined') { audience = `${projectId}.cloudfunctions.net/`; } return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience); }) .then((decoded) => { - const decodedIdToken = decoded.payload as DecodedAuthBlockingToken; - decodedIdToken.uid = decodedIdToken.sub; - return decodedIdToken; + const decodedAuthBlockingToken = decoded.payload as DecodedAuthBlockingToken; + decodedAuthBlockingToken.uid = decodedAuthBlockingToken.sub; + return decodedAuthBlockingToken; }); } - /* eslint-enable */ private ensureProjectId(): Promise { return util.findProjectId(this.app) @@ -499,17 +511,17 @@ export class FirebaseTokenVerifier { errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` + `${this.tokenInfo.shortName}, but was given a legacy custom token.`; } else { - errorMessage = 'Firebase ID token has no "kid" claim.'; + errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`; } errorMessage += verifyJwtTokenDocsMessage; } else if (!isEmulator && header.alg !== ALGORITHM_RS256) { errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; - } else if (audience && !(payload.aud as string).includes(audience)) { + } else if (typeof audience !== 'undefined' && !(payload.aud as string).includes(audience)) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage; - } else if (!audience && payload.aud !== projectId) { + } else if (typeof audience === 'undefined' && payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; @@ -582,6 +594,22 @@ export function createIdTokenVerifier(app: App): FirebaseTokenVerifier { ); } +/** + * Creates a new FirebaseTokenVerifier to verify Firebase Auth Blocking tokens. + * + * @internal + * @param app - Firebase app instance. + * @returns FirebaseTokenVerifier + */ +export function createAuthBlockingTokenVerifier(app: App): FirebaseTokenVerifier { + return new FirebaseTokenVerifier( + CLIENT_CERT_URL, + 'https://securetoken.google.com/', + AUTH_BLOCKING_TOKEN_INFO, + app + ); +} + /** * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. * diff --git a/src/utils/error.ts b/src/utils/error.ts index 10483da861..7989e7ecad 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -359,6 +359,10 @@ export class AppErrorCodes { * Auth client error codes and their default messages. */ export class AuthClientErrorCode { + public static AUTH_BLOCKING_TOKEN_EXPIRED = { + code: 'auth-blocking-token-expired', + message: 'The provided Firebase Auth Blocking token is expired.', + }; public static BILLING_NOT_ENABLED = { code: 'billing-not-enabled', message: 'Feature requires billing to be enabled.', diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 5fff2f1c95..e9a82b0581 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -213,6 +213,33 @@ export function generateIdToken(overrides?: object, claims?: object): string { return jwt.sign(payload, certificateObject.private_key, options); } +/** + * Generates a mocked Auth Blocking token. + * + * @param {object} overrides Overrides for the generated token's attributes. + * @param {object} claims Extra claims to add to the token. + * @return {string} A mocked Auth Blocking token with any provided overrides included. + */ +export function generateAuthBlockingToken(overrides?: object, claims?: object): string { + const options = _.assign({ + audience: `https://us-central1-${projectId}.cloudfunctions.net/functionName`, + expiresIn: ONE_HOUR_IN_SECONDS, + issuer: 'https://securetoken.google.com/' + projectId, + subject: uid, + algorithm: ALGORITHM, + header: { + kid: certificateObject.private_key_id, + }, + }, overrides); + + const payload = { + ...developerClaims, + ...claims, + }; + + return jwt.sign(payload, certificateObject.private_key, options); +} + /** * Generates a mocked Firebase session cookie. * diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index d0733a19f4..76997194ec 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -34,7 +34,7 @@ import { import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import * as validator from '../../../src/utils/validator'; -import { FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; +import { DecodedAuthBlockingToken, FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; import { OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from '../../../src/auth/auth-config'; @@ -166,6 +166,29 @@ function getDecodedIdToken(uid: string, authTime: Date, tenantId?: string): Deco }; } +/** + * Generates a mock decoded ID token with the provided parameters. + * + * @param {string} uid The uid corresponding to the ID token. + * @param {Date} authTime The authentication time of the ID token. + * @param {string=} tenantId The optional tenant ID. + * @return {DecodedIdToken} The generated decoded ID token. + */ +function getDecodedAuthBlockingToken(uid: string, authTime: Date): DecodedAuthBlockingToken { + return { + iss: 'https://securetoken.google.com/project123456789', + aud: 'https://us-central1-project123456789.cloudfunctions.net/functionName', + auth_time: Math.floor(authTime.getTime() / 1000), + sub: uid, + iat: Math.floor(authTime.getTime() / 1000), + exp: Math.floor(authTime.getTime() / 1000 + 3600), + uid, + event_id: 'abcdefgh', + event_type: 'beforeCreate', + ip_address: '1234556', + }; +} + /** * Generates a mock decoded session cookie with the provided parameters. @@ -422,6 +445,14 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejectedWith(expected); }); + it('_verifyAuthBlockingToken() should reject when project ID is not specified', () => { + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' + + 'as the GOOGLE_CLOUD_PROJECT environment variable to call _verifyAuthBlockingToken().'; + return mockCredentialAuth._verifyAuthBlockingToken(mocks.generateAuthBlockingToken()) + .should.eventually.be.rejectedWith(expected); + }); + describe('verifyIdToken()', () => { let stub: sinon.SinonStub; let mockIdToken: string; @@ -1005,6 +1036,101 @@ AUTH_CONFIGS.forEach((testConfig) => { } }); + describe('_verifyAuthBlockingToken()', () => { + let stub: sinon.SinonStub; + let mockAuthBlockingToken: string; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); + // Set auth_time of token to expected user's tokensValidAfterTime. + expect( + expectedUserRecord.tokensValidAfterTime, + "getValidUserRecord didn't properly set tokensValueAfterTime", + ).to.exist; + const validSince = new Date(expectedUserRecord.tokensValidAfterTime!); + // Set expected uid to expected user's. + const uid = expectedUserRecord.uid; + // Set expected decoded ID token with expected UID and auth time. + const decodedIdToken = getDecodedAuthBlockingToken(uid, validSince, tenantId); + let clock: sinon.SinonFakeTimers; + + // Stubs used to simulate underlying api calls. + const stubs: sinon.SinonStub[] = []; + beforeEach(() => { + stub = sinon.stub(FirebaseTokenVerifier.prototype, '_verifyAuthBlockingToken') + .resolves(decodedIdToken); + stubs.push(stub); + mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + clock = sinon.useFakeTimers(validSince.getTime()); + }); + afterEach(() => { + _.forEach(stubs, (s) => s.restore()); + clock.restore(); + }); + + it('should forward on the call to the token generator\'s verifyIdToken() method', () => { + // Stub getUser call. + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser'); + stubs.push(getUserStub); + return auth._verifyAuthBlockingToken(mockAuthBlockingToken).then((result) => { + // Confirm getUser never called. + expect(getUserStub).not.to.have.been.called; + expect(result).to.deep.equal(decodedIdToken); + expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); + }); + }); + + it('should reject when underlying idTokenVerifier._verifyAuthBlockingToken() rejects', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase Auth Blocking token failed'); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate ID token is invalid. + stub = sinon.stub(FirebaseTokenVerifier.prototype, '_verifyAuthBlockingToken') + .rejects(expectedError); + stubs.push(stub); + return auth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.rejectedWith('Decoding Firebase Auth Blocking token failed'); + }); + + it('should work with a non-cert credential when the GOOGLE_CLOUD_PROJECT environment variable is present', () => { + process.env.GOOGLE_CLOUD_PROJECT = mocks.projectId; + + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + + return mockCredentialAuth._verifyAuthBlockingToken(mockAuthBlockingToken).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); + }); + }); + + it('should work with a non-cert credential when the GCLOUD_PROJECT environment variable is present', () => { + process.env.GCLOUD_PROJECT = mocks.projectId; + + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + + return mockCredentialAuth._verifyAuthBlockingToken(mockAuthBlockingToken).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); + }); + }); + + it('should be fulfilled given an app which returns null access tokens', () => { + // verifyIdToken() does not rely on an access token and therefore works in this scenario. + return nullAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given an app which returns invalid access tokens', () => { + // verifyIdToken() does not rely on an access token and therefore works in this scenario. + return malformedAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given an app which fails to generate access tokens', () => { + // verifyIdToken() does not rely on an access token and therefore works in this scenario. + return rejectedPromiseAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.fulfilled; + }); + }); + describe('getUser()', () => { const uid = 'abcdefghijklmnopqrstuvwxyz'; const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 6a4b67db6a..40507083a0 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -54,10 +54,22 @@ function createTokenVerifier( ); } +function createAuthBlockingTokenVerifier( + app: FirebaseApp +): verifier.FirebaseTokenVerifier { + return new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + 'https://securetoken.google.com/', + verifier.AUTH_BLOCKING_TOKEN_INFO, + app + ); +} + describe('FirebaseTokenVerifier', () => { let app: FirebaseApp; let tokenVerifier: verifier.FirebaseTokenVerifier; + let authBlockingTokenVerifier: verifier.FirebaseTokenVerifier; let tokenGenerator: FirebaseTokenGenerator; let clock: sinon.SinonFakeTimers | undefined; beforeEach(() => { @@ -66,6 +78,7 @@ describe('FirebaseTokenVerifier', () => { const cert = new ServiceAccountCredential(mocks.certificateObject); tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); tokenVerifier = createTokenVerifier(app); + authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app); }); afterEach(() => { @@ -513,4 +526,305 @@ describe('FirebaseTokenVerifier', () => { .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim.'); }); }); + + describe('_verifyAuthBlockingToken()', () => { + let mockedRequests: nock.Scope[] = []; + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests = []; + + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should throw given no Auth Blocking JWT token', () => { + expect(() => { + (authBlockingTokenVerifier as any)._verifyAuthBlockingToken(); + }).to.throw('First argument to _verifyAuthBlockingToken() must be a Firebase Auth Blocking token'); + }); + + const invalidAuthBlockingTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAuthBlockingTokens.forEach((invalidAuthBlockingToken) => { + it('should throw given a non-string Auth Blocking JWT token: ' + JSON.stringify(invalidAuthBlockingToken), () => { + expect(() => { + authBlockingTokenVerifier._verifyAuthBlockingToken(invalidAuthBlockingToken as any, false, undefined); + }).to.throw('First argument to _verifyAuthBlockingToken() must be a Firebase Auth Blocking token'); + }); + }); + + it('should throw given an empty string Auth Blocking JWT token', () => { + return authBlockingTokenVerifier._verifyAuthBlockingToken('', false, undefined) + .should.eventually.be.rejectedWith('Decoding Firebase Auth Blocking token failed'); + }); + + it('should be rejected given an invalid Auth Blocking JWT token', () => { + return authBlockingTokenVerifier._verifyAuthBlockingToken('invalid-token', false, undefined) + .should.eventually.be.rejectedWith('Decoding Firebase Auth Blocking token failed'); + }); + + it('should throw if the token verifier was initialized with no "project_id"', () => { + const tokenVerifierWithNoProjectId = new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + 'https://securetoken.google.com/', + verifier.AUTH_BLOCKING_TOKEN_INFO, + mocks.mockCredentialApp(), + ); + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID as ' + + 'the GOOGLE_CLOUD_PROJECT environment variable to call _verifyAuthBlockingToken().'; + return tokenVerifierWithNoProjectId._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith(expected); + }); + + it('should be rejected given a Auth Blocking JWT token with no kid', () => { + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + header: { foo: 'bar' }, + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has no "kid" claim'); + }); + + it('should be rejected given a Auth Blocking JWT token with an incorrect algorithm', () => { + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + algorithm: 'HS256', + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect algorithm'); + }); + + it('should be rejected given an Auth Blocking JWT token that is not a cloud functions url', () => { + const mockAuthBlockingToken = mocks.generateIdToken({ + audience: 'incorrectAudience', + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect "aud" (audience) claim'); + }); + + it('should be rejected given a Auth Blocking JWT token with an incorrect audience', () => { + const mockAuthBlockingToken = mocks.generateIdToken({ + audience: 'https://resource-someotherurl.net/', + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, 'someurl.net/') + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect "aud" (audience) claim'); + }); + + it('should be rejected given a Auth Blocking JWT token with an incorrect issuer', () => { + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + issuer: 'incorrectIssuer', + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect "iss" (issuer) claim'); + }); + + it('should be rejected when the verifier throws no maching kid error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.NO_MATCHING_KID, 'No matching key Auth Blocking.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + header: { + kid: 'wrongkid', + }, + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has "kid" claim which does not ' + + 'correspond to a known public key'); + }); + + it('should be rejected given a Auth Blocking JWT token with a subject with greater than 128 characters', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + // uid of length 128 should be fulfilled + let uid = Array(129).join('a'); + expect(uid).to.have.length(128); + let mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + subject: uid, + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined).then(() => { + // uid of length 129 should be rejected + uid = Array(130).join('a'); + expect(uid).to.have.length(129); + mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + subject: uid, + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith( + 'Firebase Auth Blocking token has "sub" (subject) claim longer than 128 characters'); + }); + }); + + it('should be rejected when the verifier throws for expired Auth Blocking JWT token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith( + 'Firebase Auth Blocking token has expired. Get a fresh Auth Blocking token from your client ' + + 'app and try again (auth/auth-blocking-token-expired)') + .and.have.property('code', 'auth/auth-blocking-token-expired'); + }); + + it('should be rejected when the verifier throws invalid signature for a Auth Blocking JWT token.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'invalid signature.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has invalid signature'); + }); + + it('should be rejected when the verifier throws key fetch error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.KEY_FETCH_ERROR, 'Error fetching public keys.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Error fetching public keys.'); + }); + + it('should be rejected given a custom token with error using article "an" before JWT short name', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((customToken) => { + return authBlockingTokenVerifier._verifyAuthBlockingToken(customToken, false, undefined) + .should.eventually.be.rejectedWith( + '_verifyAuthBlockingToken() expects an Auth Blocking token, but was given a custom token'); + }); + }); + + it('should be rejected given a custom token with error using article "a" before JWT short name', () => { + const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', + 'https://session.firebase.google.com/', + verifier.SESSION_COOKIE_INFO, + app, + ); + return tokenGenerator.createCustomToken(mocks.uid) + .then((customToken) => { + return tokenVerifierSessionCookie._verifyAuthBlockingToken(customToken, false, undefined) + .should.eventually.be.rejectedWith( + 'verifySessionCookie() expects a session cookie, but was given a custom token'); + }); + }); + + it('should be rejected given a legacy custom token with error using article "an" before JWT short name', () => { + const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); + const legacyCustomToken = legacyTokenGenerator.createToken({ + uid: mocks.uid, + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(legacyCustomToken, false, undefined) + .should.eventually.be.rejectedWith( + '_verifyAuthBlockingToken() expects an Auth Blocking token, but was given a legacy custom token'); + }); + + it('should be rejected given a legacy custom token with error using article "a" before JWT short name', () => { + const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', + 'https://session.firebase.google.com/', + verifier.SESSION_COOKIE_INFO, + app, + ); + const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); + const legacyCustomToken = legacyTokenGenerator.createToken({ + uid: mocks.uid, + }); + + return tokenVerifierSessionCookie._verifyAuthBlockingToken(legacyCustomToken, false, undefined) + .should.eventually.be.rejectedWith( + 'verifySessionCookie() expects a session cookie, but was given a legacy custom token'); + }); + + it('AppOptions.httpAgent should be passed to the verifier', () => { + const mockAppWithAgent = mocks.appWithOptions({ + httpAgent: new Agent() + }); + const agentForApp = mockAppWithAgent.options.httpAgent; + const verifierSpy = sinon.spy(PublicKeySignatureVerifier, 'withCertificateUrl'); + + expect(verifierSpy.args).to.be.empty; + + createTokenVerifier(mockAppWithAgent); + + expect(verifierSpy.args[0][1]).to.equal(agentForApp); + verifierSpy.restore(); + }); + + it('should be fulfilled with decoded claims given a valid Auth Blocking JWT token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.fulfilled.and.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: `https://us-central1-${mocks.projectId}.cloudfunctions.net/functionName`, + iss: 'https://securetoken.google.com/' + mocks.projectId, + sub: mocks.uid, + uid: mocks.uid, + }); + }); + + it('should decode an unsigned token if isEmulator=true', async () => { + clock = sinon.useFakeTimers(1000); + + const emulatorVerifier = createTokenVerifier(app); + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + algorithm: 'none', + header: {} + }); + + const isEmulator = true; + const decoded = await emulatorVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, isEmulator, undefined); + expect(decoded).to.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: `https://us-central1-${mocks.projectId}.cloudfunctions.net/functionName`, + iss: 'https://securetoken.google.com/' + mocks.projectId, + sub: mocks.uid, + uid: mocks.uid, + }); + }); + + it('should not decode an unsigned token when the algorithm is not overridden (emulator)', async () => { + clock = sinon.useFakeTimers(1000); + + const idTokenNoAlg = mocks.generateAuthBlockingToken({ + algorithm: 'none', + }); + await authBlockingTokenVerifier._verifyAuthBlockingToken(idTokenNoAlg, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect algorithm.'); + + const idTokenNoHeader = mocks.generateAuthBlockingToken({ + algorithm: 'none', + header: {} + }); + await authBlockingTokenVerifier._verifyAuthBlockingToken(idTokenNoHeader, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has no "kid" claim.'); + }); + }); }); From a281b9b15858fc9030be1c939e0f17638dcb8883 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Fri, 8 Apr 2022 15:15:02 -0700 Subject: [PATCH 3/5] remove old ref --- test/unit/auth/auth.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 76997194ec..55f05b09e3 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -1050,7 +1050,7 @@ AUTH_CONFIGS.forEach((testConfig) => { // Set expected uid to expected user's. const uid = expectedUserRecord.uid; // Set expected decoded ID token with expected UID and auth time. - const decodedIdToken = getDecodedAuthBlockingToken(uid, validSince, tenantId); + const decodedIdToken = getDecodedAuthBlockingToken(uid, validSince); let clock: sinon.SinonFakeTimers; // Stubs used to simulate underlying api calls. From 80c058758b64ff6d70344e3b9357db69b4b54c4b Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Tue, 12 Apr 2022 11:50:25 -0700 Subject: [PATCH 4/5] adding exports --- etc/firebase-admin.api.md | 2 ++ src/auth/auth-namespace.ts | 4 +--- src/auth/token-verifier.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index ad1075cff7..33c8664d29 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -107,6 +107,8 @@ export namespace auth { // Warning: (ae-forgotten-export) The symbol "CreateTenantRequest" needs to be exported by the entry point default-namespace.d.ts export type CreateTenantRequest = CreateTenantRequest; // Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingToken" needs to be exported by the entry point default-namespace.d.ts + // + // @alpha (undocumented) export type DecodedAuthBlockingToken = DecodedAuthBlockingToken; // Warning: (ae-forgotten-export) The symbol "DecodedIdToken" needs to be exported by the entry point default-namespace.d.ts export type DecodedIdToken = DecodedIdToken; diff --git a/src/auth/auth-namespace.ts b/src/auth/auth-namespace.ts index 75c13532bb..486c0b488a 100644 --- a/src/auth/auth-namespace.ts +++ b/src/auth/auth-namespace.ts @@ -176,9 +176,7 @@ export namespace auth { */ export type DecodedIdToken = TDecodedIdToken; - /** - * Type alias to {@link firebase-admin.auth#DecodedAuthBlockingToken}. - */ + /** @alpha */ export type DecodedAuthBlockingToken = TDecodedAuthBlockingToken; /** diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index bf7581b415..e57040e4de 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -177,7 +177,7 @@ export interface DecodedIdToken { } /** @alpha */ -interface DecodedAuthBlockingSharedUserInfo { +export interface DecodedAuthBlockingSharedUserInfo { uid: string; display_name?: string; email?: string; @@ -186,18 +186,18 @@ interface DecodedAuthBlockingSharedUserInfo { } /** @alpha */ -interface DecodedAuthBlockingMetadata { +export interface DecodedAuthBlockingMetadata { creation_time?: number; last_sign_in_time?: number; } /** @alpha */ -interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo { +export interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo { provider_id: string; } /** @alpha */ -interface DecodedAuthBlockingMfaInfo { +export interface DecodedAuthBlockingMfaInfo { uid: string; display_name?: string; phone_number?: string; @@ -206,12 +206,12 @@ interface DecodedAuthBlockingMfaInfo { } /** @alpha */ -interface DecodedAuthBlockingEnrolledFactors { +export interface DecodedAuthBlockingEnrolledFactors { enrolled_factors?: DecodedAuthBlockingMfaInfo[]; } /** @alpha */ -interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo { +export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo { email_verified?: boolean; disabled?: boolean; metadata?: DecodedAuthBlockingMetadata; From 2a93113110d828d02a914e84fdbd5c58f81dd0a7 Mon Sep 17 00:00:00 2001 From: Cole Rogers Date: Wed, 13 Apr 2022 13:50:22 -0700 Subject: [PATCH 5/5] address pr comments --- test/resources/mocks.ts | 9 ++--- test/unit/auth/auth.spec.ts | 29 ++++++++------- test/unit/auth/token-verifier.spec.ts | 52 ++------------------------- 3 files changed, 22 insertions(+), 68 deletions(-) diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index e9a82b0581..f55fd9052b 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -31,6 +31,7 @@ import { Credential, GoogleOAuthAccessToken, cert } from '../../src/app/index'; const ALGORITHM = 'RS256' as const; const ONE_HOUR_IN_SECONDS = 60 * 60; +const TEN_MINUTES_IN_SECONDS = 10 * 60; export const uid = 'someUid'; export const projectId = 'project_id'; @@ -216,14 +217,14 @@ export function generateIdToken(overrides?: object, claims?: object): string { /** * Generates a mocked Auth Blocking token. * - * @param {object} overrides Overrides for the generated token's attributes. - * @param {object} claims Extra claims to add to the token. - * @return {string} A mocked Auth Blocking token with any provided overrides included. + * @param overrides Overrides for the generated token's attributes. + * @param claims Extra claims to add to the token. + * @return A mocked Auth Blocking token with any provided overrides included. */ export function generateAuthBlockingToken(overrides?: object, claims?: object): string { const options = _.assign({ audience: `https://us-central1-${projectId}.cloudfunctions.net/functionName`, - expiresIn: ONE_HOUR_IN_SECONDS, + expiresIn: TEN_MINUTES_IN_SECONDS, issuer: 'https://securetoken.google.com/' + projectId, subject: uid, algorithm: ALGORITHM, diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 55f05b09e3..1e596c2721 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -167,12 +167,11 @@ function getDecodedIdToken(uid: string, authTime: Date, tenantId?: string): Deco } /** - * Generates a mock decoded ID token with the provided parameters. + * Generates a mock decoded Auth Blocking token with the provided parameters. * - * @param {string} uid The uid corresponding to the ID token. - * @param {Date} authTime The authentication time of the ID token. - * @param {string=} tenantId The optional tenant ID. - * @return {DecodedIdToken} The generated decoded ID token. + * @param uid The uid corresponding to the Auth Blocking token. + * @param authTime The authentication time of the Auth Blocking token. + * @return The generated decoded Auth Blocking token. */ function getDecodedAuthBlockingToken(uid: string, authTime: Date): DecodedAuthBlockingToken { return { @@ -1049,15 +1048,15 @@ AUTH_CONFIGS.forEach((testConfig) => { const validSince = new Date(expectedUserRecord.tokensValidAfterTime!); // Set expected uid to expected user's. const uid = expectedUserRecord.uid; - // Set expected decoded ID token with expected UID and auth time. - const decodedIdToken = getDecodedAuthBlockingToken(uid, validSince); + // Set expected decoded Auth Blocking token with expected UID and auth time. + const decodedAuthBlockingToken = getDecodedAuthBlockingToken(uid, validSince); let clock: sinon.SinonFakeTimers; // Stubs used to simulate underlying api calls. const stubs: sinon.SinonStub[] = []; beforeEach(() => { stub = sinon.stub(FirebaseTokenVerifier.prototype, '_verifyAuthBlockingToken') - .resolves(decodedIdToken); + .resolves(decodedAuthBlockingToken); stubs.push(stub); mockAuthBlockingToken = mocks.generateAuthBlockingToken(); clock = sinon.useFakeTimers(validSince.getTime()); @@ -1067,14 +1066,14 @@ AUTH_CONFIGS.forEach((testConfig) => { clock.restore(); }); - it('should forward on the call to the token generator\'s verifyIdToken() method', () => { + it('should forward on the call to the token generator\'s _verifyAuthBlockingToken() method', () => { // Stub getUser call. const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser'); stubs.push(getUserStub); return auth._verifyAuthBlockingToken(mockAuthBlockingToken).then((result) => { // Confirm getUser never called. expect(getUserStub).not.to.have.been.called; - expect(result).to.deep.equal(decodedIdToken); + expect(result).to.deep.equal(decodedAuthBlockingToken); expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); }); }); @@ -1082,9 +1081,9 @@ AUTH_CONFIGS.forEach((testConfig) => { it('should reject when underlying idTokenVerifier._verifyAuthBlockingToken() rejects', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase Auth Blocking token failed'); - // Restore verifyIdToken stub. + // Restore _verifyAuthBlockingToken stub. stub.restore(); - // Simulate ID token is invalid. + // Simulate Auth Blocking token is invalid. stub = sinon.stub(FirebaseTokenVerifier.prototype, '_verifyAuthBlockingToken') .rejects(expectedError); stubs.push(stub); @@ -1113,19 +1112,19 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be fulfilled given an app which returns null access tokens', () => { - // verifyIdToken() does not rely on an access token and therefore works in this scenario. + // _verifyAuthBlockingToken() does not rely on an access token and therefore works in this scenario. return nullAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) .should.eventually.be.fulfilled; }); it('should be fulfilled given an app which returns invalid access tokens', () => { - // verifyIdToken() does not rely on an access token and therefore works in this scenario. + // _verifyAuthBlockingToken() does not rely on an access token and therefore works in this scenario. return malformedAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) .should.eventually.be.fulfilled; }); it('should be fulfilled given an app which fails to generate access tokens', () => { - // verifyIdToken() does not rely on an access token and therefore works in this scenario. + // _verifyAuthBlockingToken() does not rely on an access token and therefore works in this scenario. return rejectedPromiseAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) .should.eventually.be.fulfilled; }); diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 40507083a0..8496bb735c 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -42,6 +42,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; const ONE_HOUR_IN_SECONDS = 60 * 60; +const TEN_MINUTES_IN_SECONDS = 10 * 60; function createTokenVerifier( app: FirebaseApp @@ -707,21 +708,6 @@ describe('FirebaseTokenVerifier', () => { }); }); - it('should be rejected given a custom token with error using article "a" before JWT short name', () => { - const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'https://session.firebase.google.com/', - verifier.SESSION_COOKIE_INFO, - app, - ); - return tokenGenerator.createCustomToken(mocks.uid) - .then((customToken) => { - return tokenVerifierSessionCookie._verifyAuthBlockingToken(customToken, false, undefined) - .should.eventually.be.rejectedWith( - 'verifySessionCookie() expects a session cookie, but was given a custom token'); - }); - }); - it('should be rejected given a legacy custom token with error using article "an" before JWT short name', () => { const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); const legacyCustomToken = legacyTokenGenerator.createToken({ @@ -733,38 +719,6 @@ describe('FirebaseTokenVerifier', () => { '_verifyAuthBlockingToken() expects an Auth Blocking token, but was given a legacy custom token'); }); - it('should be rejected given a legacy custom token with error using article "a" before JWT short name', () => { - const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'https://session.firebase.google.com/', - verifier.SESSION_COOKIE_INFO, - app, - ); - const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); - const legacyCustomToken = legacyTokenGenerator.createToken({ - uid: mocks.uid, - }); - - return tokenVerifierSessionCookie._verifyAuthBlockingToken(legacyCustomToken, false, undefined) - .should.eventually.be.rejectedWith( - 'verifySessionCookie() expects a session cookie, but was given a legacy custom token'); - }); - - it('AppOptions.httpAgent should be passed to the verifier', () => { - const mockAppWithAgent = mocks.appWithOptions({ - httpAgent: new Agent() - }); - const agentForApp = mockAppWithAgent.options.httpAgent; - const verifierSpy = sinon.spy(PublicKeySignatureVerifier, 'withCertificateUrl'); - - expect(verifierSpy.args).to.be.empty; - - createTokenVerifier(mockAppWithAgent); - - expect(verifierSpy.args[0][1]).to.equal(agentForApp); - verifierSpy.restore(); - }); - it('should be fulfilled with decoded claims given a valid Auth Blocking JWT token', () => { const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') .resolves(); @@ -779,7 +733,7 @@ describe('FirebaseTokenVerifier', () => { one: 'uno', two: 'dos', iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, + exp: TEN_MINUTES_IN_SECONDS + 1, aud: `https://us-central1-${mocks.projectId}.cloudfunctions.net/functionName`, iss: 'https://securetoken.google.com/' + mocks.projectId, sub: mocks.uid, @@ -802,7 +756,7 @@ describe('FirebaseTokenVerifier', () => { one: 'uno', two: 'dos', iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, + exp: TEN_MINUTES_IN_SECONDS + 1, aud: `https://us-central1-${mocks.projectId}.cloudfunctions.net/functionName`, iss: 'https://securetoken.google.com/' + mocks.projectId, sub: mocks.uid,