From 8b02f7ad9a88ff6219dac20a7d71958c3df535de Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 28 Nov 2019 11:21:18 -0500 Subject: [PATCH 1/9] Allow createCustomToken() to work with tenant-aware auth --- src/auth/auth.ts | 8 +- src/auth/token-generator.ts | 37 +++- test/integration/auth.spec.ts | 43 +++++ test/unit/auth/auth.spec.ts | 82 ++++----- test/unit/auth/token-generator.spec.ts | 224 ++++++++++++++++++++++++- 5 files changed, 331 insertions(+), 63 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index e21c975637..e0815965b0 100755 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -626,7 +626,8 @@ export class TenantAwareAuth extends BaseAuth { /** * Creates a new custom token that can be sent back to a client to use with - * signInWithCustomToken(). + * signInWithCustomToken(). The tenant id will be embedded in the token and + * will be verified during the call to signInWithCustomToken(). * * @param {string} uid The uid to use as the JWT subject. * @param {object=} developerClaims Optional additional claims to include in the JWT payload. @@ -634,10 +635,7 @@ export class TenantAwareAuth extends BaseAuth { * @return {Promise} A JWT for the provided payload. */ public createCustomToken(uid: string, developerClaims?: object): Promise { - // This is not yet supported by the Auth server. It is also not yet determined how this will be - // supported. - return Promise.reject( - new FirebaseAuthError(AuthClientErrorCode.UNSUPPORTED_TENANT_OPERATION)); + return this.tokenGenerator.createCustomTokenWithTenantId(uid, this.tenantId, developerClaims); } /** diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index ea626e8c39..f39a3e75d5 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -74,6 +74,7 @@ interface JWTBody { exp: number; iss: string; sub: string; + tenant_id?: string; } /** @@ -254,13 +255,38 @@ export class FirebaseTokenGenerator { * service account key and containing the provided payload. */ public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise { + return this.createCustomTokenInternal(uid, null, developerClaims); + } + + /** + * Creates a new Firebase Auth Custom token. + * + * @param {string} uid The user ID to use for the generated Firebase Auth Custom token. + * @param {string} tenantId The tenant ID to use for the generated Firebase Auth Custom token. + * @param {object} [developerClaims] Optional developer claims to include in the generated Firebase + * Auth Custom token. + * @return {Promise} A Promise fulfilled with a Firebase Auth Custom token signed with a + * service account key and containing the provided payload. + */ + public createCustomTokenWithTenantId( + uid: string, tenantId: string, developerClaims?: {[key: string]: any}): Promise { + if (!validator.isNonEmptyString(tenantId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '`tenantId` argument must be a non-empty string uid.'); + } + return this.createCustomTokenInternal(uid, tenantId, developerClaims); + } + + private createCustomTokenInternal( + uid: string, tenantId: string | null, developerClaims?: {[key: string]: any}): Promise { let errorMessage: string; - if (typeof uid !== 'string' || uid === '') { - errorMessage = 'First argument to createCustomToken() must be a non-empty string uid.'; + if (!validator.isNonEmptyString(uid)) { + errorMessage = '`uid` argument must be a non-empty string uid.'; } else if (uid.length > 128) { - errorMessage = 'First argument to createCustomToken() must a uid with less than or equal to 128 characters.'; + errorMessage = '`uid` argument must a uid with less than or equal to 128 characters.'; } else if (!this.isDeveloperClaimsValid_(developerClaims)) { - errorMessage = 'Second argument to createCustomToken() must be an object containing the developer claims.'; + errorMessage = '`developerClaims` argument must be a valid, non-null object containing the developer claims.'; } if (typeof errorMessage !== 'undefined') { @@ -296,6 +322,9 @@ export class FirebaseTokenGenerator { sub: account, uid, }; + if (tenantId) { + body.tenant_id = tenantId; + } if (Object.keys(claims).length > 0) { body.claims = claims; } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 0f0157c131..0240d1bf90 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -710,6 +710,49 @@ describe('admin.auth', () => { expect(userRecord.uid).to.equal(createdUserUid); }); }); + + it('createCustomToken() mints a JWT that can be used to sign in tenant users', () => { + return tenantAwareAuth.createCustomToken('uid1') + .then((customToken) => { + firebase.auth().tenantId = createdTenantId; + return firebase.auth().signInWithCustomToken(customToken); + }) + .then(({user}) => { + return user.getIdToken(); + }) + .then((idToken) => { + return tenantAwareAuth.verifyIdToken(idToken); + }) + .then((token) => { + expect(token.uid).to.equal('uid1'); + expect(token.firebase.tenant).to.equal(createdTenantId); + }); + }); + + it('createCustomToken() should fail if tenantIds mismatch', async () => { + const aDifferentTenant = await admin.auth().tenantManager().createTenant({displayName: 'A-Different-Tenant'}); + try { + const customToken = await tenantAwareAuth.createCustomToken('uid1'); + + firebase.auth().tenantId = aDifferentTenant.tenantId; + + await expect(firebase.auth().signInWithCustomToken(customToken)) + .to.be.rejectedWith('Specified tenant ID does not match the custom token.'); + + // TODO(rsgowman): Currently, the above error has a code of + // 'auth/internal-error', i.e. you could add the following chain onto + // it: + // + // .and.eventually.have.property('code', 'auth/internal-error'); + // + // However, this doesn't strike me as an internal error, implying + // that the client sdk is slightly "wrong". Fix the client sdk to + // return a better error code, and then add the above statement to + // the expect statement (with the new error code.) + } finally { + admin.auth().tenantManager().deleteTenant(aDifferentTenant.tenantId); + } + }); }); // Sanity check OIDC/SAML config management API. diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 8793caf5bc..71e269c916 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -16,6 +16,7 @@ 'use strict'; +import * as jwt from 'jsonwebtoken'; import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; @@ -28,7 +29,6 @@ import * as mocks from '../../resources/mocks'; import {Auth, TenantAwareAuth, BaseAuth, DecodedIdToken} from '../../../src/auth/auth'; import {UserRecord} from '../../../src/auth/user-record'; import {FirebaseApp} from '../../../src/firebase-app'; -import {FirebaseTokenGenerator} from '../../../src/auth/token-generator'; import { AuthRequestHandler, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; @@ -328,63 +328,49 @@ AUTH_CONFIGS.forEach((testConfig) => { } describe('createCustomToken()', () => { - let spy: sinon.SinonSpy; - beforeEach(() => { - spy = sinon.spy(FirebaseTokenGenerator.prototype, 'createCustomToken'); - }); - - afterEach(() => { - spy.restore(); + it('should return a jwt', async () => { + const token = await auth.createCustomToken('uid1'); + const decodedToken = jwt.decode(token, {complete: true}); + expect(decodedToken).to.have.property('header').that.has.property('typ', 'JWT'); }); if (testConfig.Auth === TenantAwareAuth) { - it('should reject with an unsupported tenant operation error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.UNSUPPORTED_TENANT_OPERATION); - return auth.createCustomToken(mocks.uid) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.deep.equal(expectedError); - }); + it('should contain tenant_id', async () => { + const token = await auth.createCustomToken('uid1'); + expect(jwt.decode(token)).to.have.property('tenant_id', TENANT_ID); }); } else { - it('should throw if a cert credential is not specified', () => { - const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - - expect(() => { - mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims); - }).not.to.throw; + it('should not contain tenant_id', async () => { + const token = await auth.createCustomToken('uid1'); + expect(jwt.decode(token)).to.not.have.property('tenant_id'); }); + } - it('should forward on the call to the token generator\'s createCustomToken() method', () => { - const developerClaimsCopy = deepCopy(mocks.developerClaims); - return auth.createCustomToken(mocks.uid, mocks.developerClaims) - .then(() => { - expect(spy) - .to.have.been.calledOnce - .and.calledWith(mocks.uid, developerClaimsCopy); - }); - }); + it('should throw if a cert credential is not specified', () => { + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should be fulfilled given an app which returns null access tokens', () => { - // createCustomToken() does not rely on an access token and therefore works in this scenario. - return nullAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; - }); + expect(() => { + mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims); + }).not.to.throw; + }); - it('should be fulfilled given an app which returns invalid access tokens', () => { - // createCustomToken() does not rely on an access token and therefore works in this scenario. - return malformedAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; - }); + it('should be fulfilled given an app which returns null access tokens', () => { + // createCustomToken() does not rely on an access token and therefore works in this scenario. + return nullAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) + .should.eventually.be.fulfilled; + }); - it('should be fulfilled given an app which fails to generate access tokens', () => { - // createCustomToken() does not rely on an access token and therefore works in this scenario. - return rejectedPromiseAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; - }); - } + it('should be fulfilled given an app which returns invalid access tokens', () => { + // createCustomToken() does not rely on an access token and therefore works in this scenario. + return malformedAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given an app which fails to generate access tokens', () => { + // createCustomToken() does not rely on an access token and therefore works in this scenario. + return rejectedPromiseAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) + .should.eventually.be.fulfilled; + }); }); it('verifyIdToken() should throw when project ID is not specified', () => { diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 072d6c2509..b2f1e9a891 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -30,6 +30,7 @@ import {Certificate} from '../../../src/auth/credential'; import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; import { FirebaseApp } from '../../../src/firebase-app'; import * as utils from '../utils'; +import { FirebaseAuthError } from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); @@ -293,7 +294,7 @@ describe('FirebaseTokenGenerator', () => { it('should throw given no uid', () => { expect(() => { (tokenGenerator as any).createCustomToken(); - }).to.throw('First argument to createCustomToken() must be a non-empty string uid'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; @@ -301,14 +302,14 @@ describe('FirebaseTokenGenerator', () => { it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { expect(() => { tokenGenerator.createCustomToken(invalidUid as any); - }).to.throw('First argument to createCustomToken() must be a non-empty string uid'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); }); it('should throw given an empty string uid', () => { expect(() => { tokenGenerator.createCustomToken(''); - }).to.throw('First argument to createCustomToken() must be a non-empty string uid'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); it('should throw given a uid with a length greater than 128 characters', () => { @@ -324,7 +325,7 @@ describe('FirebaseTokenGenerator', () => { expect(uid).to.have.length(129); expect(() => { tokenGenerator.createCustomToken(uid); - }).to.throw('First argument to createCustomToken() must a uid with less than or equal to 128 characters'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); it('should throw given a non-object developer claims', () => { @@ -332,7 +333,7 @@ describe('FirebaseTokenGenerator', () => { invalidDeveloperClaims.forEach((invalidDevClaims) => { expect(() => { tokenGenerator.createCustomToken(mocks.uid, invalidDevClaims); - }).to.throw('Second argument to createCustomToken() must be an object containing the developer claims'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); }); @@ -342,7 +343,7 @@ describe('FirebaseTokenGenerator', () => { blacklistedDeveloperClaims[blacklistedClaim] = true; expect(() => { tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); - }).to.throw('Developer claim "' + blacklistedClaim + '" is reserved and cannot be specified'); + }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); }); }); @@ -466,4 +467,215 @@ describe('FirebaseTokenGenerator', () => { }); }); }); + + describe('createCustomTokenWithTenantId()', () => { + it('should throw given no uid', () => { + expect(() => { + (tokenGenerator as any).createCustomTokenWithTenantId(); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('should throw given no tenantId', () => { + expect(() => { + (tokenGenerator as any).createCustomTokenWithTenantId('uid1'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidUids.forEach((invalidUid) => { + it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { + expect(() => { + tokenGenerator.createCustomTokenWithTenantId(invalidUid as any, 'tenantid1'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + invalidUids.forEach((invalidUid) => { + it('should throw given a non-string tenantId: ' + JSON.stringify(invalidUid), () => { + expect(() => { + tokenGenerator.createCustomTokenWithTenantId('uid1', invalidUid as any); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + + it('should throw given an empty string uid', () => { + expect(() => { + tokenGenerator.createCustomTokenWithTenantId ('', 'tenantid1'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('should throw given an empty string tenantId', () => { + expect(() => { + tokenGenerator.createCustomTokenWithTenantId('uid1', ''); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('should throw given a uid with a length greater than 128 characters', () => { + // uid of length 128 should be allowed + let uid = Array(129).join('a'); + expect(uid).to.have.length(128); + expect(() => { + tokenGenerator.createCustomTokenWithTenantId(uid, 'tenantid1'); + }).not.to.throw(); + + // uid of length 129 should throw + uid = Array(130).join('a'); + expect(uid).to.have.length(129); + expect(() => { + tokenGenerator.createCustomTokenWithTenantId(uid, 'tenantid1'); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + // Note: We don't enforce a max length on tenantid. + + it('should throw given a non-object developer claims', () => { + const invalidDeveloperClaims: any[] = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop]; + invalidDeveloperClaims.forEach((invalidDevClaims) => { + expect(() => { + tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', invalidDevClaims); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + + BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { + it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { + const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); + blacklistedDeveloperClaims[blacklistedClaim] = true; + expect(() => { + tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); + }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); + }); + }); + + BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { + it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { + const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); + blacklistedDeveloperClaims[blacklistedClaim] = true; + expect(() => { + tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', blacklistedDeveloperClaims); + }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); + }); + }); + + it('should be fulfilled given a valid uid, tenantId and no developer claims', () => { + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1'); + }); + + it('should be fulfilled given a valid uid, tenantId empty object developer claims', () => { + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', {}); + }); + + it('should be fulfilled given a valid uid, tenantId and valid developer claims', () => { + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', mocks.developerClaims); + }); + + it('should be fulfilled with a Firebase Custom JWT', () => { + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') + .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); + + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') + .then((token) => { + const decoded = jwt.decode(token); + + expect(decoded).to.deep.equal({ + uid: 'uid1', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + tenant_id: 'tenantid1', + }); + }); + }); + + it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', mocks.developerClaims) + .then((token) => { + const decoded = jwt.decode(token); + + expect(decoded).to.deep.equal({ + uid: 'uid1', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + tenant_id: 'tenantid1', + claims: { + one: 'uno', + two: 'dos', + }, + }); + }); + }); + + it('should be fulfilled with a JWT with the correct header', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid') + .then((token) => { + const decoded: any = jwt.decode(token, { + complete: true, + }); + expect(decoded.header).to.deep.equal({ + alg: ALGORITHM, + typ: 'JWT', + }); + }); + }); + + it('should be fulfilled with a JWT which can be verified by the service account public key', () => { + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') + .then((token) => { + return verifyToken(token, mocks.keyPairs[0].public); + }); + }); + + it('should be fulfilled with a JWT which cannot be verified by a random public key', () => { + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') + .then((token) => { + return verifyToken(token, mocks.keyPairs[1].public) + .should.eventually.be.rejectedWith('invalid signature'); + }); + }); + + it('should be fulfilled with a JWT which expires after one hour', () => { + clock = sinon.useFakeTimers(1000); + + let token: string; + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') + .then((result) => { + token = result; + + clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + + // Token should still be valid + return verifyToken(token, mocks.keyPairs[0].public); + }) + .then(() => { + clock.tick(1); + + // Token should now be invalid + return verifyToken(token, mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt expired'); + }); + }); + + it('should not mutate the passed in developer claims', () => { + const originalClaims = { + foo: 'bar', + }; + const clonedClaims = _.clone(originalClaims); + return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', clonedClaims) + .then(() => { + expect(originalClaims).to.deep.equal(clonedClaims); + }); + }); + }); }); From 4648c081ecd3ffcd99a47edbed740fdc6c17a9e3 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 5 Dec 2019 11:33:49 -0500 Subject: [PATCH 2/9] Missed an await() --- test/integration/auth.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 0240d1bf90..376ae58784 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -750,7 +750,7 @@ describe('admin.auth', () => { // return a better error code, and then add the above statement to // the expect statement (with the new error code.) } finally { - admin.auth().tenantManager().deleteTenant(aDifferentTenant.tenantId); + await admin.auth().tenantManager().deleteTenant(aDifferentTenant.tenantId); } }); }); From 08dfb17438e7ddc89e5491e4c88f34d0c36b7ad3 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Fri, 6 Dec 2019 11:51:22 -0500 Subject: [PATCH 3/9] remove unnecessary test --- test/integration/auth.spec.ts | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 376ae58784..cb8547d4bd 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -728,31 +728,6 @@ describe('admin.auth', () => { expect(token.firebase.tenant).to.equal(createdTenantId); }); }); - - it('createCustomToken() should fail if tenantIds mismatch', async () => { - const aDifferentTenant = await admin.auth().tenantManager().createTenant({displayName: 'A-Different-Tenant'}); - try { - const customToken = await tenantAwareAuth.createCustomToken('uid1'); - - firebase.auth().tenantId = aDifferentTenant.tenantId; - - await expect(firebase.auth().signInWithCustomToken(customToken)) - .to.be.rejectedWith('Specified tenant ID does not match the custom token.'); - - // TODO(rsgowman): Currently, the above error has a code of - // 'auth/internal-error', i.e. you could add the following chain onto - // it: - // - // .and.eventually.have.property('code', 'auth/internal-error'); - // - // However, this doesn't strike me as an internal error, implying - // that the client sdk is slightly "wrong". Fix the client sdk to - // return a better error code, and then add the above statement to - // the expect statement (with the new error code.) - } finally { - await admin.auth().tenantManager().deleteTenant(aDifferentTenant.tenantId); - } - }); }); // Sanity check OIDC/SAML config management API. From e7490599d6ad97ca3d52347bcd24ca324c8e270c Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Tue, 10 Dec 2019 14:50:23 -0500 Subject: [PATCH 4/9] review feedback --- src/auth/token-generator.ts | 22 +++++++++++----------- test/unit/auth/auth.spec.ts | 7 +++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 062a1175f0..3bffaa83cd 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -261,11 +261,11 @@ export class FirebaseTokenGenerator { /** * Creates a new Firebase Auth Custom token. * - * @param {string} uid The user ID to use for the generated Firebase Auth Custom token. - * @param {object} [developerClaims] Optional developer claims to include in the generated Firebase - * Auth Custom token. - * @return {Promise} A Promise fulfilled with a Firebase Auth Custom token signed with a - * service account key and containing the provided payload. + * @param uid The user ID to use for the generated Firebase Auth Custom token. + * @param developerClaims Optional developer claims to include in the generated Firebase + * Auth Custom token. + * @return A Promise fulfilled with a Firebase Auth Custom token signed with a + * service account key and containing the provided payload. */ public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise { return this.createCustomTokenInternal(uid, null, developerClaims); @@ -274,12 +274,12 @@ export class FirebaseTokenGenerator { /** * Creates a new Firebase Auth Custom token. * - * @param {string} uid The user ID to use for the generated Firebase Auth Custom token. - * @param {string} tenantId The tenant ID to use for the generated Firebase Auth Custom token. - * @param {object} [developerClaims] Optional developer claims to include in the generated Firebase - * Auth Custom token. - * @return {Promise} A Promise fulfilled with a Firebase Auth Custom token signed with a - * service account key and containing the provided payload. + * @param uid The user ID to use for the generated Firebase Auth Custom token. + * @param tenantId The tenant ID to use for the generated Firebase Auth Custom token. + * @param developerClaims Optional developer claims to include in the generated Firebase + * Auth Custom token. + * @return A Promise fulfilled with a Firebase Auth Custom token signed with + * a service account key and containing the provided payload. */ public createCustomTokenWithTenantId( uid: string, tenantId: string, developerClaims?: {[key: string]: any}): Promise { diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 8015f0f0e6..86a9ca63e4 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -346,12 +346,11 @@ AUTH_CONFIGS.forEach((testConfig) => { }); } - it('should throw if a cert credential is not specified', () => { + it('should be fulfilled even if a cert credential is not specified', () => { const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - expect(() => { - mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims); - }).not.to.throw; + return mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims) + .should.eventually.be.fulfilled; }); it('should be fulfilled given an app which returns null access tokens', () => { From a971e4a930e5ed5b9806c2144cc98cf6c7ee8f56 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Tue, 10 Dec 2019 16:16:52 -0500 Subject: [PATCH 5/9] oops --- test/unit/auth/auth.spec.ts | 4 ++-- test/unit/auth/token-generator.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 86a9ca63e4..c41e8a124c 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -346,11 +346,11 @@ AUTH_CONFIGS.forEach((testConfig) => { }); } - it('should be fulfilled even if a cert credential is not specified', () => { + it('should be eventually rejected if a cert credential is not specified', () => { const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); return mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-credential'); }); it('should be fulfilled given an app which returns null access tokens', () => { diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 4f123e17ae..8920d17065 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -651,13 +651,13 @@ describe('FirebaseTokenGenerator', () => { .then((result) => { token = result; - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); // Token should still be valid return verifyToken(token, mocks.keyPairs[0].public); }) .then(() => { - clock.tick(1); + clock!.tick(1); // Token should now be invalid return verifyToken(token, mocks.keyPairs[0].public) From f9d8d66dd67e415e4ae82ef998213ab4a46dc38b Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Wed, 11 Dec 2019 14:21:27 -0500 Subject: [PATCH 6/9] Option c: Tenant aware TokenGenerator. Cuts down on the test duplication. --- src/auth/auth.ts | 28 +- src/auth/token-generator.ts | 41 +- src/index.d.ts | 3 +- test/integration/auth.spec.ts | 31 +- test/unit/auth/token-generator.spec.ts | 515 ++++++++----------------- 5 files changed, 209 insertions(+), 409 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 22e18cb007..d361e15f03 100755 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -102,9 +102,13 @@ export class BaseAuth { * for this instance. * @constructor */ - constructor(app: FirebaseApp, protected readonly authRequestHandler: T) { - const cryptoSigner = cryptoSignerFromApp(app); - this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); + constructor(app: FirebaseApp, protected readonly authRequestHandler: T, tokenGenerator?: FirebaseTokenGenerator) { + if (tokenGenerator) { + this.tokenGenerator = tokenGenerator; + } else { + const cryptoSigner = cryptoSignerFromApp(app); + this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); + } const projectId = utils.getProjectId(app); const httpAgent = app.options.httpAgent; @@ -616,24 +620,12 @@ export class TenantAwareAuth extends BaseAuth { * @constructor */ constructor(app: FirebaseApp, tenantId: string) { - super(app, new TenantAwareAuthRequestHandler(app, tenantId)); + const cryptoSigner = cryptoSignerFromApp(app); + const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId); + super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator); utils.addReadonlyGetter(this, 'tenantId', tenantId); } - /** - * Creates a new custom token that can be sent back to a client to use with - * signInWithCustomToken(). The tenant id will be embedded in the token and - * will be verified during the call to signInWithCustomToken(). - * - * @param {string} uid The uid to use as the JWT subject. - * @param {object=} developerClaims Optional additional claims to include in the JWT payload. - * - * @return {Promise} A JWT for the provided payload. - */ - public createCustomToken(uid: string, developerClaims?: object): Promise { - return this.tokenGenerator.createCustomTokenWithTenantId(uid, this.tenantId, developerClaims); - } - /** * Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects * the promise if the token could not be verified. If checkRevoked is set to true, diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 3bffaa83cd..801fcf24c8 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -248,13 +248,23 @@ export class FirebaseTokenGenerator { private readonly signer: CryptoSigner; - constructor(signer: CryptoSigner) { + /** + * @param tenantId The tenant ID to use for the generated Firebase Auth + * Custom token. If absent, then no tenant ID claim will be set in the + * resulting JWT. + */ + constructor(signer: CryptoSigner, public readonly tenantId?: string) { if (!validator.isNonNullObject(signer)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, 'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', ); } + if (tenantId !== undefined && !validator.isNonEmptyString(tenantId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '`tenantId` argument must be a non-empty string uid.'); + } this.signer = signer; } @@ -268,31 +278,6 @@ export class FirebaseTokenGenerator { * service account key and containing the provided payload. */ public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise { - return this.createCustomTokenInternal(uid, null, developerClaims); - } - - /** - * Creates a new Firebase Auth Custom token. - * - * @param uid The user ID to use for the generated Firebase Auth Custom token. - * @param tenantId The tenant ID to use for the generated Firebase Auth Custom token. - * @param developerClaims Optional developer claims to include in the generated Firebase - * Auth Custom token. - * @return A Promise fulfilled with a Firebase Auth Custom token signed with - * a service account key and containing the provided payload. - */ - public createCustomTokenWithTenantId( - uid: string, tenantId: string, developerClaims?: {[key: string]: any}): Promise { - if (!validator.isNonEmptyString(tenantId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - '`tenantId` argument must be a non-empty string uid.'); - } - return this.createCustomTokenInternal(uid, tenantId, developerClaims); - } - - private createCustomTokenInternal( - uid: string, tenantId: string | null, developerClaims?: {[key: string]: any}): Promise { let errorMessage: string | undefined; if (!validator.isNonEmptyString(uid)) { errorMessage = '`uid` argument must be a non-empty string uid.'; @@ -335,8 +320,8 @@ export class FirebaseTokenGenerator { sub: account, uid, }; - if (tenantId) { - body.tenant_id = tenantId; + if (this.tenantId) { + body.tenant_id = this.tenantId; } if (Object.keys(claims).length > 0) { body.claims = claims; diff --git a/src/index.d.ts b/src/index.d.ts index 74b74de941..6461389195 100755 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1445,7 +1445,8 @@ declare namespace admin.auth { /** * Creates a new Firebase custom token (JWT) that can be sent back to a client * device to use to sign in with the client SDKs' `signInWithCustomToken()` - * methods. + * methods. (Tenant-aware instances will also embed the tenant ID in the + * token.) * * See [Create Custom Tokens](/docs/auth/admin/create-custom-tokens) for code * samples and detailed documentation. diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index ae78255f97..f2038be62f 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -723,22 +723,21 @@ describe('admin.auth', () => { }); }); - it('createCustomToken() mints a JWT that can be used to sign in tenant users', () => { - return tenantAwareAuth.createCustomToken('uid1') - .then((customToken) => { - firebase.auth().tenantId = createdTenantId; - return firebase.auth().signInWithCustomToken(customToken); - }) - .then(({user}) => { - return user.getIdToken(); - }) - .then((idToken) => { - return tenantAwareAuth.verifyIdToken(idToken); - }) - .then((token) => { - expect(token.uid).to.equal('uid1'); - expect(token.firebase.tenant).to.equal(createdTenantId); - }); + it('createCustomToken() mints a JWT that can be used to sign in tenant users', async () => { + try { + firebase.auth!().tenantId = createdTenantId; + + const customToken = await tenantAwareAuth.createCustomToken('uid1'); + const {user} = await firebase.auth!().signInWithCustomToken(customToken); + expect(user).to.not.be.null; + const idToken = await user!.getIdToken(); + const token = await tenantAwareAuth.verifyIdToken(idToken); + + expect(token.uid).to.equal('uid1'); + expect(token.firebase.tenant).to.equal(createdTenantId); + } finally { + firebase.auth!().tenantId = null; + } }); }); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 8920d17065..eca50d8a2e 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -254,14 +254,10 @@ describe('CryptoSigner', () => { }); describe('FirebaseTokenGenerator', () => { - let tokenGenerator: FirebaseTokenGenerator; + const tenantId = 'tenantId1'; + const cert = new Certificate(mocks.certificateObject); let clock: sinon.SinonFakeTimers | undefined; - beforeEach(() => { - const cert = new Certificate(mocks.certificateObject); - tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); - }); - afterEach(() => { if (clock) { clock.restore(); @@ -286,394 +282,221 @@ describe('FirebaseTokenGenerator', () => { }).to.throw('Must provide a CryptoSigner to use FirebaseTokenGenerator'); }); }); - }); - describe('createCustomToken()', () => { - it('should throw given no uid', () => { - expect(() => { - (tokenGenerator as any).createCustomToken(); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); - - const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; - invalidUids.forEach((invalidUid) => { - it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { + const invalidTenantIds = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should throw given a non-string tenantId', () => { expect(() => { - tokenGenerator.createCustomToken(invalidUid as any); + return new FirebaseTokenGenerator(new ServiceAccountSigner(cert), invalidTenantId as any); }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); }); - it('should throw given an empty string uid', () => { + it('should throw given an empty string tenantId', () => { expect(() => { - tokenGenerator.createCustomToken(''); + return new FirebaseTokenGenerator(new ServiceAccountSigner(cert), ''); }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); + }); - it('should throw given a uid with a length greater than 128 characters', () => { - // uid of length 128 should be allowed - let uid = Array(129).join('a'); - expect(uid).to.have.length(128); - expect(() => { - tokenGenerator.createCustomToken(uid); - }).not.to.throw(); + const tokenGeneratorConfigs = [{ + name: 'createCustomToken()', + tokenGenerator: new FirebaseTokenGenerator(new ServiceAccountSigner(cert)), + }, { + name: 'createCustomToken() (tenant-aware)', + tokenGenerator: new FirebaseTokenGenerator(new ServiceAccountSigner(cert), tenantId), + }]; - // uid of length 129 should throw - uid = Array(130).join('a'); - expect(uid).to.have.length(129); - expect(() => { - tokenGenerator.createCustomToken(uid); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); + tokenGeneratorConfigs.forEach((tokenGeneratorConfig) => { + describe(tokenGeneratorConfig.name, () => { + const tokenGenerator = tokenGeneratorConfig.tokenGenerator; - it('should throw given a non-object developer claims', () => { - const invalidDeveloperClaims: any[] = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop]; - invalidDeveloperClaims.forEach((invalidDevClaims) => { + it('should throw given no uid', () => { expect(() => { - tokenGenerator.createCustomToken(mocks.uid, invalidDevClaims); + (tokenGenerator as any).createCustomToken(); }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - - BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { - it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { - const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); - blacklistedDeveloperClaims[blacklistedClaim] = true; - expect(() => { - tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); - }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); - }); - }); - it('should be fulfilled given a valid uid and no developer claims', () => { - return tokenGenerator.createCustomToken(mocks.uid); - }); - - it('should be fulfilled given a valid uid and empty object developer claims', () => { - return tokenGenerator.createCustomToken(mocks.uid, {}); - }); - - it('should be fulfilled given a valid uid and valid developer claims', () => { - return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims); - }); - - it('should be fulfilled with a Firebase Custom JWT', () => { - return tokenGenerator.createCustomToken(mocks.uid) - .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); - - return tokenGenerator.createCustomToken(mocks.uid) - .then((token) => { - const decoded = jwt.decode(token); - - expect(decoded).to.deep.equal({ - uid: mocks.uid, - iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, - aud: FIREBASE_AUDIENCE, - iss: mocks.certificateObject.client_email, - sub: mocks.certificateObject.client_email, - }); - }); - }); - - it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => { - clock = sinon.useFakeTimers(1000); - - return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims) - .then((token) => { - const decoded = jwt.decode(token); - - expect(decoded).to.deep.equal({ - uid: mocks.uid, - iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, - aud: FIREBASE_AUDIENCE, - iss: mocks.certificateObject.client_email, - sub: mocks.certificateObject.client_email, - claims: { - one: 'uno', - two: 'dos', - }, - }); + const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidUids.forEach((invalidUid) => { + it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { + expect(() => { + tokenGenerator.createCustomToken(invalidUid as any); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - - it('should be fulfilled with a JWT with the correct header', () => { - clock = sinon.useFakeTimers(1000); - - return tokenGenerator.createCustomToken(mocks.uid) - .then((token) => { - const decoded: any = jwt.decode(token, { - complete: true, - }); - expect(decoded.header).to.deep.equal({ - alg: ALGORITHM, - typ: 'JWT', - }); - }); - }); - - it('should be fulfilled with a JWT which can be verified by the service account public key', () => { - return tokenGenerator.createCustomToken(mocks.uid) - .then((token) => { - return verifyToken(token, mocks.keyPairs[0].public); - }); - }); - - it('should be fulfilled with a JWT which cannot be verified by a random public key', () => { - return tokenGenerator.createCustomToken(mocks.uid) - .then((token) => { - return verifyToken(token, mocks.keyPairs[1].public) - .should.eventually.be.rejectedWith('invalid signature'); - }); - }); - - it('should be fulfilled with a JWT which expires after one hour', () => { - clock = sinon.useFakeTimers(1000); - - let token: string; - return tokenGenerator.createCustomToken(mocks.uid) - .then((result) => { - token = result; - - clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); - - // Token should still be valid - return verifyToken(token, mocks.keyPairs[0].public); - }) - .then(() => { - clock!.tick(1); - - // Token should now be invalid - return verifyToken(token, mocks.keyPairs[0].public) - .should.eventually.be.rejectedWith('jwt expired'); - }); - }); - - it('should not mutate the passed in developer claims', () => { - const originalClaims = { - foo: 'bar', - }; - const clonedClaims = _.clone(originalClaims); - return tokenGenerator.createCustomToken(mocks.uid, clonedClaims) - .then(() => { - expect(originalClaims).to.deep.equal(clonedClaims); - }); - }); - }); - - describe('createCustomTokenWithTenantId()', () => { - it('should throw given no uid', () => { - expect(() => { - (tokenGenerator as any).createCustomTokenWithTenantId(); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); - - it('should throw given no tenantId', () => { - expect(() => { - (tokenGenerator as any).createCustomTokenWithTenantId('uid1'); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); - - const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; - invalidUids.forEach((invalidUid) => { - it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { - expect(() => { - tokenGenerator.createCustomTokenWithTenantId(invalidUid as any, 'tenantid1'); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - invalidUids.forEach((invalidUid) => { - it('should throw given a non-string tenantId: ' + JSON.stringify(invalidUid), () => { + + it('should throw given an empty string uid', () => { expect(() => { - tokenGenerator.createCustomTokenWithTenantId('uid1', invalidUid as any); + tokenGenerator.createCustomToken(''); }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - it('should throw given an empty string uid', () => { - expect(() => { - tokenGenerator.createCustomTokenWithTenantId ('', 'tenantid1'); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); - - it('should throw given an empty string tenantId', () => { - expect(() => { - tokenGenerator.createCustomTokenWithTenantId('uid1', ''); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); - - it('should throw given a uid with a length greater than 128 characters', () => { - // uid of length 128 should be allowed - let uid = Array(129).join('a'); - expect(uid).to.have.length(128); - expect(() => { - tokenGenerator.createCustomTokenWithTenantId(uid, 'tenantid1'); - }).not.to.throw(); - - // uid of length 129 should throw - uid = Array(130).join('a'); - expect(uid).to.have.length(129); - expect(() => { - tokenGenerator.createCustomTokenWithTenantId(uid, 'tenantid1'); - }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); - }); - - // Note: We don't enforce a max length on tenantid. + it('should throw given a uid with a length greater than 128 characters', () => { + // uid of length 128 should be allowed + let uid = Array(129).join('a'); + expect(uid).to.have.length(128); + expect(() => { + tokenGenerator.createCustomToken(uid); + }).not.to.throw(); - it('should throw given a non-object developer claims', () => { - const invalidDeveloperClaims: any[] = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop]; - invalidDeveloperClaims.forEach((invalidDevClaims) => { + // uid of length 129 should throw + uid = Array(130).join('a'); + expect(uid).to.have.length(129); expect(() => { - tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', invalidDevClaims); + tokenGenerator.createCustomToken(uid); }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { - it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { - const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); - blacklistedDeveloperClaims[blacklistedClaim] = true; - expect(() => { - tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); - }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); + it('should throw given a non-object developer claims', () => { + const invalidDeveloperClaims: any[] = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop]; + invalidDeveloperClaims.forEach((invalidDevClaims) => { + expect(() => { + tokenGenerator.createCustomToken(mocks.uid, invalidDevClaims); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); }); - }); - BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { - it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { - const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); - blacklistedDeveloperClaims[blacklistedClaim] = true; - expect(() => { - tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', blacklistedDeveloperClaims); - }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); + BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { + it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { + const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); + blacklistedDeveloperClaims[blacklistedClaim] = true; + expect(() => { + tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); + }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); + }); }); - }); - it('should be fulfilled given a valid uid, tenantId and no developer claims', () => { - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1'); - }); + it('should be fulfilled given a valid uid and no developer claims', () => { + return tokenGenerator.createCustomToken(mocks.uid); + }); - it('should be fulfilled given a valid uid, tenantId empty object developer claims', () => { - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', {}); - }); + it('should be fulfilled given a valid uid and empty object developer claims', () => { + return tokenGenerator.createCustomToken(mocks.uid, {}); + }); - it('should be fulfilled given a valid uid, tenantId and valid developer claims', () => { - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', mocks.developerClaims); - }); + it('should be fulfilled given a valid uid and valid developer claims', () => { + return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims); + }); - it('should be fulfilled with a Firebase Custom JWT', () => { - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') - .should.eventually.be.a('string').and.not.be.empty; - }); + it('should be fulfilled with a Firebase Custom JWT', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .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); - - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') - .then((token) => { - const decoded = jwt.decode(token); - - expect(decoded).to.deep.equal({ - uid: 'uid1', - iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, - aud: FIREBASE_AUDIENCE, - iss: mocks.certificateObject.client_email, - sub: mocks.certificateObject.client_email, - tenant_id: 'tenantid1', + it('should be fulfilled with a JWT with the correct decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + const decoded = jwt.decode(token); + const expected: {[key: string]: any} = { + uid: mocks.uid, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + }; + + if (tokenGenerator.tenantId) { + expected.tenant_id = tokenGenerator.tenantId; + } + + expect(decoded).to.deep.equal(expected); }); - }); - }); + }); - it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => { - clock = sinon.useFakeTimers(1000); - - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', mocks.developerClaims) - .then((token) => { - const decoded = jwt.decode(token); - - expect(decoded).to.deep.equal({ - uid: 'uid1', - iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, - aud: FIREBASE_AUDIENCE, - iss: mocks.certificateObject.client_email, - sub: mocks.certificateObject.client_email, - tenant_id: 'tenantid1', - claims: { - one: 'uno', - two: 'dos', - }, + it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims) + .then((token) => { + const decoded = jwt.decode(token); + + const expected: {[key: string]: any} = { + uid: mocks.uid, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + claims: { + one: 'uno', + two: 'dos', + }, + }; + + if (tokenGenerator.tenantId) { + expected.tenant_id = tokenGenerator.tenantId; + } + + expect(decoded).to.deep.equal(expected); }); - }); - }); - - it('should be fulfilled with a JWT with the correct header', () => { - clock = sinon.useFakeTimers(1000); + }); - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid') - .then((token) => { - const decoded: any = jwt.decode(token, { - complete: true, - }); - expect(decoded.header).to.deep.equal({ - alg: ALGORITHM, - typ: 'JWT', + it('should be fulfilled with a JWT with the correct header', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + const decoded: any = jwt.decode(token, { + complete: true, + }); + expect(decoded.header).to.deep.equal({ + alg: ALGORITHM, + typ: 'JWT', + }); }); - }); - }); + }); - it('should be fulfilled with a JWT which can be verified by the service account public key', () => { - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') - .then((token) => { - return verifyToken(token, mocks.keyPairs[0].public); - }); - }); + it('should be fulfilled with a JWT which can be verified by the service account public key', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + return verifyToken(token, mocks.keyPairs[0].public); + }); + }); - it('should be fulfilled with a JWT which cannot be verified by a random public key', () => { - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') - .then((token) => { - return verifyToken(token, mocks.keyPairs[1].public) - .should.eventually.be.rejectedWith('invalid signature'); - }); - }); + it('should be fulfilled with a JWT which cannot be verified by a random public key', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + return verifyToken(token, mocks.keyPairs[1].public) + .should.eventually.be.rejectedWith('invalid signature'); + }); + }); - it('should be fulfilled with a JWT which expires after one hour', () => { - clock = sinon.useFakeTimers(1000); + it('should be fulfilled with a JWT which expires after one hour', () => { + clock = sinon.useFakeTimers(1000); - let token: string; - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1') - .then((result) => { - token = result; + let token: string; + return tokenGenerator.createCustomToken(mocks.uid) + .then((result) => { + token = result; - clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); - // Token should still be valid - return verifyToken(token, mocks.keyPairs[0].public); - }) - .then(() => { - clock!.tick(1); + // Token should still be valid + return verifyToken(token, mocks.keyPairs[0].public); + }) + .then(() => { + clock!.tick(1); - // Token should now be invalid - return verifyToken(token, mocks.keyPairs[0].public) - .should.eventually.be.rejectedWith('jwt expired'); - }); - }); + // Token should now be invalid + return verifyToken(token, mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt expired'); + }); + }); - it('should not mutate the passed in developer claims', () => { - const originalClaims = { - foo: 'bar', - }; - const clonedClaims = _.clone(originalClaims); - return tokenGenerator.createCustomTokenWithTenantId('uid1', 'tenantid1', clonedClaims) - .then(() => { - expect(originalClaims).to.deep.equal(clonedClaims); - }); + it('should not mutate the passed in developer claims', () => { + const originalClaims = { + foo: 'bar', + }; + const clonedClaims = _.clone(originalClaims); + return tokenGenerator.createCustomToken(mocks.uid, clonedClaims) + .then(() => { + expect(originalClaims).to.deep.equal(clonedClaims); + }); + }); }); }); }); From f6937e45be80cd68aff5e8953788b38014bfa05c Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 12 Dec 2019 11:49:03 -0500 Subject: [PATCH 7/9] more review feedback --- src/auth/auth.ts | 7 +++++-- src/auth/token-generator.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index d26ad68c5e..eddea69cb3 100755 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -98,8 +98,11 @@ export class BaseAuth { /** * The BaseAuth class constructor. * - * @param {T} authRequestHandler The RPC request handler - * for this instance. + * @param app The FirebaseApp to associate with this Auth instance. + * @param authRequestHandler The RPC request handler for this instance. + * @param tokenGenerator Optional token generator. If not specified, a + * (non-tenant-aware) instance will be created. Use this paramter to + * specify a tenant-aware tokenGenerator. * @constructor */ constructor(app: FirebaseApp, protected readonly authRequestHandler: T, tokenGenerator?: FirebaseTokenGenerator) { diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 801fcf24c8..28d76a43b4 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -260,10 +260,10 @@ export class FirebaseTokenGenerator { 'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', ); } - if (tenantId !== undefined && !validator.isNonEmptyString(tenantId)) { + if (typeof tenantId !== 'undefined' && !validator.isNonEmptyString(tenantId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '`tenantId` argument must be a non-empty string uid.'); + '`tenantId` argument must be a non-empty string.'); } this.signer = signer; } From 0bccb66e8b3a4888d83529ff6fcba068b08cbde4 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Thu, 12 Dec 2019 12:00:27 -0500 Subject: [PATCH 8/9] firebase.auth!() -> client_auth() --- test/integration/auth.spec.ts | 48 +++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index f2038be62f..b8c1fb39c8 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -30,7 +30,7 @@ import url = require('url'); import * as mocks from '../resources/mocks'; import { AuthProviderConfig } from '../../src/auth/auth-config'; import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; -import { User } from '@firebase/auth-types'; +import { User, FirebaseAuth } from '@firebase/auth-types'; /* tslint:disable:no-var-requires */ const chalk = require('chalk'); @@ -91,6 +91,10 @@ function randomOidcProviderId(): string { return 'oidc.' + generateRandomString(10, false).toLowerCase(); } +function client_auth(): FirebaseAuth { + expect(firebase.auth).to.be.ok; + return firebase.auth!(); +} describe('admin.auth', () => { @@ -213,7 +217,7 @@ describe('admin.auth', () => { let currentIdToken: string; let currentUser: User; // Sign in with an email and password account. - return firebase.auth!().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) + return client_auth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) .then(({user}) => { expect(user).to.exist; currentUser = user!; @@ -248,7 +252,7 @@ describe('admin.auth', () => { }) .then(() => { // New sign-in should succeed. - return firebase.auth!().signInWithEmailAndPassword( + return client_auth().signInWithEmailAndPassword( mockUserData.email, mockUserData.password); }) .then(({user}) => { @@ -273,7 +277,7 @@ describe('admin.auth', () => { // Confirm custom claims set on the UserRecord. expect(userRecord.customClaims).to.deep.equal(customClaims); expect(userRecord.email).to.exist; - return firebase.auth!().signInWithEmailAndPassword( + return client_auth().signInWithEmailAndPassword( userRecord.email!, mockUserData.password); }) .then(({user}) => { @@ -302,8 +306,8 @@ describe('admin.auth', () => { // Custom claims should be cleared. expect(userRecord.customClaims).to.deep.equal({}); // Force token refresh. All claims should be cleared. - expect(firebase.auth!().currentUser).to.exist; - return firebase.auth!().currentUser!.getIdToken(true); + expect(client_auth().currentUser).to.exist; + return client_auth().currentUser!.getIdToken(true); }) .then((idToken) => { // Verify ID token contents. @@ -368,7 +372,7 @@ describe('admin.auth', () => { isAdmin: true, }) .then((customToken) => { - return firebase.auth!().signInWithCustomToken(customToken); + return client_auth().signInWithCustomToken(customToken); }) .then(({user}) => { expect(user).to.exist; @@ -388,7 +392,7 @@ describe('admin.auth', () => { isAdmin: true, }) .then((customToken) => { - return firebase.auth!().signInWithCustomToken(customToken); + return client_auth().signInWithCustomToken(customToken); }) .then(({user}) => { expect(user).to.exist; @@ -426,7 +430,7 @@ describe('admin.auth', () => { // Sign out after each test. afterEach(() => { - return firebase.auth!().signOut(); + return client_auth().signOut(); }); // Delete test user at the end of test suite. @@ -443,10 +447,10 @@ describe('admin.auth', () => { .then((link) => { const code = getActionCode(link); expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return firebase.auth!().confirmPasswordReset(code, newPassword); + return client_auth().confirmPasswordReset(code, newPassword); }) .then(() => { - return firebase.auth!().signInWithEmailAndPassword(email, newPassword); + return client_auth().signInWithEmailAndPassword(email, newPassword); }) .then((result) => { expect(result.user).to.exist; @@ -466,10 +470,10 @@ describe('admin.auth', () => { .then((link) => { const code = getActionCode(link); expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return firebase.auth!().applyActionCode(code); + return client_auth().applyActionCode(code); }) .then(() => { - return firebase.auth!().signInWithEmailAndPassword(email, userData.password); + return client_auth().signInWithEmailAndPassword(email, userData.password); }) .then((result) => { expect(result.user).to.exist; @@ -482,7 +486,7 @@ describe('admin.auth', () => { return admin.auth().generateSignInWithEmailLink(email, actionCodeSettings) .then((link) => { expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return firebase.auth!().signInWithEmailLink(email, link); + return client_auth().signInWithEmailLink(email, link); }) .then((result) => { expect(result.user).to.exist; @@ -725,10 +729,10 @@ describe('admin.auth', () => { it('createCustomToken() mints a JWT that can be used to sign in tenant users', async () => { try { - firebase.auth!().tenantId = createdTenantId; + client_auth().tenantId = createdTenantId; const customToken = await tenantAwareAuth.createCustomToken('uid1'); - const {user} = await firebase.auth!().signInWithCustomToken(customToken); + const {user} = await client_auth().signInWithCustomToken(customToken); expect(user).to.not.be.null; const idToken = await user!.getIdToken(); const token = await tenantAwareAuth.verifyIdToken(idToken); @@ -736,7 +740,7 @@ describe('admin.auth', () => { expect(token.uid).to.equal('uid1'); expect(token.firebase.tenant).to.equal(createdTenantId); } finally { - firebase.auth!().tenantId = null; + client_auth().tenantId = null; } }); }); @@ -1220,7 +1224,7 @@ describe('admin.auth', () => { it('creates a valid Firebase session cookie', () => { return admin.auth().createCustomToken(uid, {admin: true, groupId: '1234'}) - .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then((customToken) => client_auth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1256,7 +1260,7 @@ describe('admin.auth', () => { it('creates a revocable session cookie', () => { let currentSessionCookie: string; return admin.auth().createCustomToken(uid2) - .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then((customToken) => client_auth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1283,7 +1287,7 @@ describe('admin.auth', () => { it('fails when called with a revoked ID token', () => { return admin.auth().createCustomToken(uid3, {admin: true, groupId: '1234'}) - .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then((customToken) => client_auth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1311,7 +1315,7 @@ describe('admin.auth', () => { it('fails when called with a Firebase ID token', () => { return admin.auth().createCustomToken(uid) - .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then((customToken) => client_auth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1597,7 +1601,7 @@ function testImportAndSignInUser( expect(result.successCount).to.equal(1); expect(result.errors.length).to.equal(0); // Sign in with an email and password to the imported account. - return firebase.auth!().signInWithEmailAndPassword(users[0].email, rawPassword); + return client_auth().signInWithEmailAndPassword(users[0].email, rawPassword); }) .then(({user}) => { // Confirm successful sign-in. From 0067272c8515af78c3eaeec9570adeddc48158e9 Mon Sep 17 00:00:00 2001 From: Rich Gowman Date: Fri, 13 Dec 2019 09:53:35 -0500 Subject: [PATCH 9/9] client_auth => clientAuth --- test/integration/auth.spec.ts | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index b8c1fb39c8..b78e1e6c43 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -91,7 +91,7 @@ function randomOidcProviderId(): string { return 'oidc.' + generateRandomString(10, false).toLowerCase(); } -function client_auth(): FirebaseAuth { +function clientAuth(): FirebaseAuth { expect(firebase.auth).to.be.ok; return firebase.auth!(); } @@ -217,7 +217,7 @@ describe('admin.auth', () => { let currentIdToken: string; let currentUser: User; // Sign in with an email and password account. - return client_auth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) + return clientAuth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) .then(({user}) => { expect(user).to.exist; currentUser = user!; @@ -252,7 +252,7 @@ describe('admin.auth', () => { }) .then(() => { // New sign-in should succeed. - return client_auth().signInWithEmailAndPassword( + return clientAuth().signInWithEmailAndPassword( mockUserData.email, mockUserData.password); }) .then(({user}) => { @@ -277,7 +277,7 @@ describe('admin.auth', () => { // Confirm custom claims set on the UserRecord. expect(userRecord.customClaims).to.deep.equal(customClaims); expect(userRecord.email).to.exist; - return client_auth().signInWithEmailAndPassword( + return clientAuth().signInWithEmailAndPassword( userRecord.email!, mockUserData.password); }) .then(({user}) => { @@ -306,8 +306,8 @@ describe('admin.auth', () => { // Custom claims should be cleared. expect(userRecord.customClaims).to.deep.equal({}); // Force token refresh. All claims should be cleared. - expect(client_auth().currentUser).to.exist; - return client_auth().currentUser!.getIdToken(true); + expect(clientAuth().currentUser).to.exist; + return clientAuth().currentUser!.getIdToken(true); }) .then((idToken) => { // Verify ID token contents. @@ -372,7 +372,7 @@ describe('admin.auth', () => { isAdmin: true, }) .then((customToken) => { - return client_auth().signInWithCustomToken(customToken); + return clientAuth().signInWithCustomToken(customToken); }) .then(({user}) => { expect(user).to.exist; @@ -392,7 +392,7 @@ describe('admin.auth', () => { isAdmin: true, }) .then((customToken) => { - return client_auth().signInWithCustomToken(customToken); + return clientAuth().signInWithCustomToken(customToken); }) .then(({user}) => { expect(user).to.exist; @@ -430,7 +430,7 @@ describe('admin.auth', () => { // Sign out after each test. afterEach(() => { - return client_auth().signOut(); + return clientAuth().signOut(); }); // Delete test user at the end of test suite. @@ -447,10 +447,10 @@ describe('admin.auth', () => { .then((link) => { const code = getActionCode(link); expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return client_auth().confirmPasswordReset(code, newPassword); + return clientAuth().confirmPasswordReset(code, newPassword); }) .then(() => { - return client_auth().signInWithEmailAndPassword(email, newPassword); + return clientAuth().signInWithEmailAndPassword(email, newPassword); }) .then((result) => { expect(result.user).to.exist; @@ -470,10 +470,10 @@ describe('admin.auth', () => { .then((link) => { const code = getActionCode(link); expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return client_auth().applyActionCode(code); + return clientAuth().applyActionCode(code); }) .then(() => { - return client_auth().signInWithEmailAndPassword(email, userData.password); + return clientAuth().signInWithEmailAndPassword(email, userData.password); }) .then((result) => { expect(result.user).to.exist; @@ -486,7 +486,7 @@ describe('admin.auth', () => { return admin.auth().generateSignInWithEmailLink(email, actionCodeSettings) .then((link) => { expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return client_auth().signInWithEmailLink(email, link); + return clientAuth().signInWithEmailLink(email, link); }) .then((result) => { expect(result.user).to.exist; @@ -729,10 +729,10 @@ describe('admin.auth', () => { it('createCustomToken() mints a JWT that can be used to sign in tenant users', async () => { try { - client_auth().tenantId = createdTenantId; + clientAuth().tenantId = createdTenantId; const customToken = await tenantAwareAuth.createCustomToken('uid1'); - const {user} = await client_auth().signInWithCustomToken(customToken); + const {user} = await clientAuth().signInWithCustomToken(customToken); expect(user).to.not.be.null; const idToken = await user!.getIdToken(); const token = await tenantAwareAuth.verifyIdToken(idToken); @@ -740,7 +740,7 @@ describe('admin.auth', () => { expect(token.uid).to.equal('uid1'); expect(token.firebase.tenant).to.equal(createdTenantId); } finally { - client_auth().tenantId = null; + clientAuth().tenantId = null; } }); }); @@ -1224,7 +1224,7 @@ describe('admin.auth', () => { it('creates a valid Firebase session cookie', () => { return admin.auth().createCustomToken(uid, {admin: true, groupId: '1234'}) - .then((customToken) => client_auth().signInWithCustomToken(customToken)) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1260,7 +1260,7 @@ describe('admin.auth', () => { it('creates a revocable session cookie', () => { let currentSessionCookie: string; return admin.auth().createCustomToken(uid2) - .then((customToken) => client_auth().signInWithCustomToken(customToken)) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1287,7 +1287,7 @@ describe('admin.auth', () => { it('fails when called with a revoked ID token', () => { return admin.auth().createCustomToken(uid3, {admin: true, groupId: '1234'}) - .then((customToken) => client_auth().signInWithCustomToken(customToken)) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1315,7 +1315,7 @@ describe('admin.auth', () => { it('fails when called with a Firebase ID token', () => { return admin.auth().createCustomToken(uid) - .then((customToken) => client_auth().signInWithCustomToken(customToken)) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) .then(({user}) => { expect(user).to.exist; return user!.getIdToken(); @@ -1601,7 +1601,7 @@ function testImportAndSignInUser( expect(result.successCount).to.equal(1); expect(result.errors.length).to.equal(0); // Sign in with an email and password to the imported account. - return client_auth().signInWithEmailAndPassword(users[0].email, rawPassword); + return clientAuth().signInWithEmailAndPassword(users[0].email, rawPassword); }) .then(({user}) => { // Confirm successful sign-in.