From c5dbbb5394ab6959fd56f7c24957eeeb8d987d6f Mon Sep 17 00:00:00 2001 From: Xin Li Date: Wed, 4 Aug 2021 16:51:28 -0700 Subject: [PATCH 01/13] fix: Throw error on user disabled and check revoked set true --- src/auth/auth.ts | 51 +++++++-- src/utils/error.ts | 4 + test/integration/auth.spec.ts | 189 +++++++++++++++++++++++++++------- test/unit/auth/auth.spec.ts | 50 +++++++-- 4 files changed, 241 insertions(+), 53 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 48f46c345a..3673c788d1 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -105,10 +105,13 @@ export class BaseAuth implements BaseAuthI /** * 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, - * verifies if the session corresponding to the ID token was revoked. If the corresponding - * user's session was invalidated, an auth/id-token-revoked error is thrown. If not specified - * the check is not applied. + * the promise if the token could not be verified. + * If checkRevoked is set to true, first verifies whether the corresponding userinfo.disabled + * is true: + * If yes, an auth/user-disabled error is thrown. + * If no, verifies if the session corresponding to the ID token was revoked. + * If the corresponding user's session was invalidated, an auth/id-token-revoked error is thrown. + * If not specified the check is not applied. * * @param {string} idToken The JWT to verify. * @param {boolean=} checkRevoked Whether to check if the ID token is revoked. @@ -121,6 +124,20 @@ export class BaseAuth implements BaseAuthI .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. if (checkRevoked || isEmulator) { + // Whether user has been disabled. + if (checkRevoked) { + const uid = decodedIdToken.uid; + return this.getUser(uid) + .then((userInfo: UserRecord) => { + if (userInfo.disabled) { + throw new FirebaseAuthError( + AuthClientErrorCode.USER_DISABLED, 'The user account has been disabled by an administrator'); + } + return this.verifyDecodedJWTNotRevoked( + decodedIdToken, + AuthClientErrorCode.ID_TOKEN_REVOKED); + }); + } return this.verifyDecodedJWTNotRevoked( decodedIdToken, AuthClientErrorCode.ID_TOKEN_REVOKED); @@ -507,10 +524,14 @@ export class BaseAuth implements BaseAuthI /** * Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects - * the promise if the token could not be verified. If checkRevoked is set to true, - * verifies if the session corresponding to the session cookie was revoked. If the corresponding - * user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not - * specified the check is not performed. + * the promise if the token could not be verified. + * If checkRevoked is set to true, first verifies whether the corresponding userinfo.disabled + * is true: + * If yes, an auth/user-disabled error is thrown. + * If no, verifies if the session corresponding to the ID token was revoked. + * If the corresponding user's session was invalidated, an auth/session-cookie-revoked error + * is thrown. + * If not specified the check is not performed. * * @param {string} sessionCookie The session cookie to verify. * @param {boolean=} checkRevoked Whether to check if the session cookie is revoked. @@ -524,6 +545,20 @@ export class BaseAuth implements BaseAuthI .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. if (checkRevoked || isEmulator) { + // Whether user has been disabled. + if (checkRevoked) { + const uid = decodedIdToken.uid; + return this.getUser(uid) + .then((userInfo: UserRecord) => { + if (userInfo.disabled) { + throw new FirebaseAuthError( + AuthClientErrorCode.USER_DISABLED, 'The user account has been disabled by an administrator'); + } + return this.verifyDecodedJWTNotRevoked( + decodedIdToken, + AuthClientErrorCode.SESSION_COOKIE_REVOKED); + }); + } return this.verifyDecodedJWTNotRevoked( decodedIdToken, AuthClientErrorCode.SESSION_COOKIE_REVOKED); diff --git a/src/utils/error.ts b/src/utils/error.ts index fcec6d5fd3..b6c51946f4 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -721,6 +721,10 @@ export class AuthClientErrorCode { code: 'not-found', message: 'The requested resource was not found.', }; + public static USER_DISABLED = { + code: 'user-disabled', + message: 'The user account has been disabled by an administrator.', + } public static USER_NOT_DISABLED = { code: 'user-not-disabled', message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).', diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 55b78fa232..5e96df842e 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -833,53 +833,120 @@ describe('admin.auth', () => { }); }); - it('verifyIdToken() fails when called with an invalid token', () => { - return admin.auth().verifyIdToken('invalid-token') - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); + describe('verifyIdToken()', () => { + const uid = generateRandomString(20).toLowerCase(); + const email = uid + '@example.com'; + const password = 'password'; + const userData = { + uid, + email, + emailVerified: false, + password, + }; + + // Create the test user before running this suite of tests. + before(() => { + return admin.auth().createUser(userData); + }); + + // Sign out after each test. + afterEach(() => { + return clientAuth().signOut(); + }); + + after(() => { + return safeDelete(uid); + }); - if (authEmulatorHost) { - describe('Auth emulator support', () => { - const uid = 'authEmulatorUser'; - before(() => { - return admin.auth().createUser({ - uid, - email: 'lastRefreshTimeUser@example.com', - password: 'p4ssword', + it('verifyIdToken() fails when called with an invalid token', () => { + return admin.auth().verifyIdToken('invalid-token') + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('verifyIdToken() fails with checkRevoked set to true and corresponding user disabled', () => { + let currentIdToken: string; + return clientAuth().signInWithEmailAndPassword(email, password) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + return result.user!.getIdToken() + }) + .then((idToken) => { + currentIdToken = idToken; + return admin.auth().verifyIdToken(currentIdToken, true) + }) + .then((decodedIdToken) => { + expect(decodedIdToken.uid).to.equal(uid); + expect(decodedIdToken.email).to.equal(email); + return admin.auth().updateUser(uid, { disabled: true }) + }) + .then((userRecord) => { + // Ensure disabled field has been updated. + expect(userRecord.uid).to.equal(uid); + expect(userRecord.email).to.equal(email); + expect(userRecord.disabled).to.equal(true); + return clientAuth().signInWithEmailAndPassword(email, password) + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + return result.user!.getIdToken() + }) + .then((idToken) => { + // Ensure idToken is the same before verifying. + expect(currentIdToken).to.equal(idToken); + return admin.auth().verifyIdToken(idToken, true) + }) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.have.property('code', 'auth/user-disabled'); }); - }); - after(() => { - return admin.auth().deleteUser(uid); - }); + }); - it('verifyIdToken() succeeds when called with an unsigned token', () => { - const unsignedToken = mocks.generateIdToken({ - algorithm: 'none', - audience: projectId, - issuer: 'https://securetoken.google.com/' + projectId, - subject: uid, + if (authEmulatorHost) { + describe('Auth emulator support', () => { + const uid = 'authEmulatorUser'; + before(() => { + return admin.auth().createUser({ + uid, + email: 'lastRefreshTimeUser@example.com', + password: 'p4ssword', + }); + }); + after(() => { + return admin.auth().deleteUser(uid); }); - return admin.auth().verifyIdToken(unsignedToken); - }); - it('verifyIdToken() fails when called with a token with wrong project', () => { - const unsignedToken = mocks.generateIdToken({ algorithm: 'none', audience: 'nosuch' }); - return admin.auth().verifyIdToken(unsignedToken) - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); + it('verifyIdToken() succeeds when called with an unsigned token', () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + audience: projectId, + issuer: 'https://securetoken.google.com/' + projectId, + subject: uid, + }); + return admin.auth().verifyIdToken(unsignedToken); + }); - it('verifyIdToken() fails when called with a token that does not belong to a user', () => { - const unsignedToken = mocks.generateIdToken({ - algorithm: 'none', - audience: projectId, - issuer: 'https://securetoken.google.com/' + projectId, - subject: 'nosuch', + it('verifyIdToken() fails when called with a token with wrong project', () => { + const unsignedToken = mocks.generateIdToken({ algorithm: 'none', audience: 'nosuch' }); + return admin.auth().verifyIdToken(unsignedToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('verifyIdToken() fails when called with a token that does not belong to a user', () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + audience: projectId, + issuer: 'https://securetoken.google.com/' + projectId, + subject: 'nosuch', + }); + return admin.auth().verifyIdToken(unsignedToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); - return admin.auth().verifyIdToken(unsignedToken) - .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); - }); - } + } + }); describe('Link operations', () => { const uid = generateRandomString(20).toLowerCase(); @@ -1982,6 +2049,50 @@ describe('admin.auth', () => { .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); }); + + it('fails with checkRevoked set to true and corresponding user disabled', () => { + const expiresIn = 24 * 60 * 60 * 1000; + let currentIdToken: string; + let currentCustomToken: string; + let currentSessioncookie: string; + return admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }) + .then((customToken) => { + currentCustomToken = customToken; + return clientAuth().signInWithCustomToken(customToken) + }) + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); + }) + .then((idToken) => { + currentIdToken = idToken; + return admin.auth().verifyIdToken(idToken); + }).then((decodedIdTokenClaims) => { + expect(decodedIdTokenClaims.uid).to.be.equal(uid); + // One day long session cookie. + return admin.auth().createSessionCookie(currentIdToken, { expiresIn }); + }) + .then((sessionCookie) => { + currentSessioncookie = sessionCookie; + admin.auth().verifySessionCookie(sessionCookie, true) + }) + .then(() => { + return admin.auth().updateUser(uid, { disabled : true }); + }) + .then((userRecord) => { + // Ensure disabled field has been updated. + expect(userRecord.uid).to.equal(uid); + expect(userRecord.disabled).to.equal(true); + return clientAuth().signInWithCustomToken(currentCustomToken) + }).then(() => { + admin.auth().verifySessionCookie(currentSessioncookie, true) + }) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); }); describe('importUsers()', () => { diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 71957bc4b4..9299766c91 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -482,6 +482,25 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejectedWith('Decoding Firebase ID token failed'); }); + it('should be rejected with checkRevoked set to true and corresponding user disabled', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + expectedAccountInfoResponse.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponse); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + return auth.verifyIdToken(mockIdToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); + it('should work with a non-cert credential when the GOOGLE_CLOUD_PROJECT environment variable is present', () => { process.env.GOOGLE_CLOUD_PROJECT = mocks.projectId; @@ -528,7 +547,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifyIdToken(mockIdToken, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); expect(result).to.deep.equal(decodedIdToken); }); }); @@ -551,7 +570,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); // Confirm expected error returned. expect(error).to.have.property('code', 'auth/id-token-revoked'); }); @@ -610,7 +629,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifyIdToken(mockIdToken, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); expect(result).to.deep.equal(decodedIdToken); }); }); @@ -772,7 +791,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifySessionCookie(mockSessionCookie, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); expect(result).to.deep.equal(decodedSessionCookie); }); }); @@ -795,7 +814,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); // Confirm expected error returned. expect(error).to.have.property('code', 'auth/session-cookie-revoked'); }); @@ -838,6 +857,25 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + it('should be rejected with checkRevoked set to true and corresponding user disabled', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + expectedAccountInfoResponse.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponse); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + return auth.verifyIdToken(mockSessionCookie, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); + it('should be fulfilled with checkRevoked set to true when no validSince available', () => { // Simulate no validSince set on the user. const noValidSinceGetAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); @@ -854,7 +892,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifySessionCookie(mockSessionCookie, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); expect(result).to.deep.equal(decodedSessionCookie); }); }); From d7703f0f54510b8d73dfcc0f8782a96b94ee7b29 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Mon, 9 Aug 2021 17:13:09 -0700 Subject: [PATCH 02/13] resolve 2 calls on getUser(), improve tests and lints --- src/auth/auth.ts | 53 ++++++++++--------------------- src/utils/error.ts | 2 +- test/integration/auth.spec.ts | 60 +++++++++++++++++++++++------------ test/unit/auth/auth.spec.ts | 12 +++---- 4 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 3673c788d1..477ccd6e7b 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -124,21 +124,7 @@ export class BaseAuth implements BaseAuthI .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. if (checkRevoked || isEmulator) { - // Whether user has been disabled. - if (checkRevoked) { - const uid = decodedIdToken.uid; - return this.getUser(uid) - .then((userInfo: UserRecord) => { - if (userInfo.disabled) { - throw new FirebaseAuthError( - AuthClientErrorCode.USER_DISABLED, 'The user account has been disabled by an administrator'); - } - return this.verifyDecodedJWTNotRevoked( - decodedIdToken, - AuthClientErrorCode.ID_TOKEN_REVOKED); - }); - } - return this.verifyDecodedJWTNotRevoked( + return this.verifyDecodedJWTNotRevokedOrDisabled( decodedIdToken, AuthClientErrorCode.ID_TOKEN_REVOKED); } @@ -524,11 +510,11 @@ export class BaseAuth implements BaseAuthI /** * Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects - * the promise if the token could not be verified. + * the promise if the cookie could not be verified. * If checkRevoked is set to true, first verifies whether the corresponding userinfo.disabled * is true: * If yes, an auth/user-disabled error is thrown. - * If no, verifies if the session corresponding to the ID token was revoked. + * If no, verifies if the session corresponding to the session cookie was revoked. * If the corresponding user's session was invalidated, an auth/session-cookie-revoked error * is thrown. * If not specified the check is not performed. @@ -543,23 +529,9 @@ export class BaseAuth implements BaseAuthI const isEmulator = useEmulator(); return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) .then((decodedIdToken: DecodedIdToken) => { - // Whether to check if the token was revoked. - if (checkRevoked || isEmulator) { - // Whether user has been disabled. - if (checkRevoked) { - const uid = decodedIdToken.uid; - return this.getUser(uid) - .then((userInfo: UserRecord) => { - if (userInfo.disabled) { - throw new FirebaseAuthError( - AuthClientErrorCode.USER_DISABLED, 'The user account has been disabled by an administrator'); - } - return this.verifyDecodedJWTNotRevoked( - decodedIdToken, - AuthClientErrorCode.SESSION_COOKIE_REVOKED); - }); - } - return this.verifyDecodedJWTNotRevoked( + // Whether to check if the cookie was revoked. + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevokedOrDisabled( decodedIdToken, AuthClientErrorCode.SESSION_COOKIE_REVOKED); } @@ -758,8 +730,9 @@ export class BaseAuth implements BaseAuthI } /** - * Verifies the decoded Firebase issued JWT is not revoked. Returns a promise that resolves - * with the decoded claims on success. Rejects the promise with revocation error if revoked. + * 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 + * or user disabled. * * @param {DecodedIdToken} decodedIdToken The JWT's decoded claims. * @param {ErrorInfo} revocationErrorInfo The revocation error info to throw on revocation @@ -767,11 +740,17 @@ export class BaseAuth implements BaseAuthI * @return {Promise} A Promise that will be fulfilled after a successful * verification. */ - private verifyDecodedJWTNotRevoked( + private verifyDecodedJWTNotRevokedOrDisabled( decodedIdToken: DecodedIdToken, revocationErrorInfo: ErrorInfo): Promise { // Get tokens valid after time for the corresponding user. return this.getUser(decodedIdToken.sub) .then((user: UserRecord) => { + // If user disabled, throw an auth/user-disabled error. + if (user.disabled) { + throw new FirebaseAuthError( + AuthClientErrorCode.USER_DISABLED, + 'The user account has been disabled by an administrator'); + } // If no tokens valid after time available, token is not revoked. if (user.tokensValidAfterTime) { // Get the ID token authentication time and convert to milliseconds UTC. diff --git a/src/utils/error.ts b/src/utils/error.ts index b6c51946f4..de22a4b9ca 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -723,7 +723,7 @@ export class AuthClientErrorCode { }; public static USER_DISABLED = { code: 'user-disabled', - message: 'The user account has been disabled by an administrator.', + message: 'The user record is disabled.', } public static USER_NOT_DISABLED = { code: 'user-not-disabled', diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 5e96df842e..ef5915b3d2 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -760,6 +760,20 @@ describe('admin.auth', () => { safeDelete(userRecord.uid); } }); + + it('A user with user record disabled is unable to sign in', async () => { + const password = 'password'; + const email = 'updatedEmail@example.com'; + return admin.auth().updateUser(updateUser.uid, { disabled : true , password, email }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, password); + }) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); }); it('getUser() fails when called with a non-existing UID', () => { @@ -865,37 +879,37 @@ describe('admin.auth', () => { it('verifyIdToken() fails with checkRevoked set to true and corresponding user disabled', () => { let currentIdToken: string; + let currentUser: User; return clientAuth().signInWithEmailAndPassword(email, password) - .then((result) => { - expect(result.user).to.exist; - expect(result.user!.email).to.equal(email); - return result.user!.getIdToken() + .then(({ user }) => { + expect(user).to.exist; + expect(user!.email).to.equal(email); + currentUser = user!; + return user!.getIdToken(); }) .then((idToken) => { currentIdToken = idToken; - return admin.auth().verifyIdToken(currentIdToken, true) + return admin.auth().verifyIdToken(currentIdToken, true); }) .then((decodedIdToken) => { expect(decodedIdToken.uid).to.equal(uid); expect(decodedIdToken.email).to.equal(email); - return admin.auth().updateUser(uid, { disabled: true }) + return admin.auth().updateUser(uid, { disabled: true }); }) .then((userRecord) => { // Ensure disabled field has been updated. expect(userRecord.uid).to.equal(uid); expect(userRecord.email).to.equal(email); expect(userRecord.disabled).to.equal(true); - return clientAuth().signInWithEmailAndPassword(email, password) + return admin.auth().verifyIdToken(currentIdToken, false); }) - .then((result) => { - expect(result.user).to.exist; - expect(result.user!.email).to.equal(email); - return result.user!.getIdToken() + .then(() => { + // Verification should be successful. + // Need to reload user to ensure currentUser been updated. + return currentUser.reload(); }) - .then((idToken) => { - // Ensure idToken is the same before verifying. - expect(currentIdToken).to.equal(idToken); - return admin.auth().verifyIdToken(idToken, true) + .then(() => { + return admin.auth().verifyIdToken(currentIdToken, true); }) .then(() => { throw new Error('Unexpected success'); @@ -2053,15 +2067,15 @@ describe('admin.auth', () => { it('fails with checkRevoked set to true and corresponding user disabled', () => { const expiresIn = 24 * 60 * 60 * 1000; let currentIdToken: string; - let currentCustomToken: string; let currentSessioncookie: string; + let currentUser: User; return admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }) .then((customToken) => { - currentCustomToken = customToken; - return clientAuth().signInWithCustomToken(customToken) + return clientAuth().signInWithCustomToken(customToken); }) .then(({ user }) => { expect(user).to.exist; + currentUser = user!; return user!.getIdToken(); }) .then((idToken) => { @@ -2074,7 +2088,7 @@ describe('admin.auth', () => { }) .then((sessionCookie) => { currentSessioncookie = sessionCookie; - admin.auth().verifySessionCookie(sessionCookie, true) + return admin.auth().verifySessionCookie(sessionCookie, true); }) .then(() => { return admin.auth().updateUser(uid, { disabled : true }); @@ -2083,9 +2097,13 @@ describe('admin.auth', () => { // Ensure disabled field has been updated. expect(userRecord.uid).to.equal(uid); expect(userRecord.disabled).to.equal(true); - return clientAuth().signInWithCustomToken(currentCustomToken) + return admin.auth().verifySessionCookie(currentSessioncookie, false); + }) + .then(() => { + // Need to reload user to ensure currentUser been updated. + return currentUser.reload(); }).then(() => { - admin.auth().verifySessionCookie(currentSessioncookie, true) + admin.auth().verifySessionCookie(currentSessioncookie, true); }) .then(() => { throw new Error('Unexpected success'); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 9299766c91..795ff8926a 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -547,7 +547,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifyIdToken(mockIdToken, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); expect(result).to.deep.equal(decodedIdToken); }); }); @@ -570,7 +570,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); // Confirm expected error returned. expect(error).to.have.property('code', 'auth/id-token-revoked'); }); @@ -629,7 +629,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifyIdToken(mockIdToken, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); expect(result).to.deep.equal(decodedIdToken); }); }); @@ -791,7 +791,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifySessionCookie(mockSessionCookie, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); expect(result).to.deep.equal(decodedSessionCookie); }); }); @@ -814,7 +814,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); // Confirm expected error returned. expect(error).to.have.property('code', 'auth/session-cookie-revoked'); }); @@ -892,7 +892,7 @@ AUTH_CONFIGS.forEach((testConfig) => { return auth.verifySessionCookie(mockSessionCookie, true) .then((result) => { // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledTwice.and.calledWith(uid); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); expect(result).to.deep.equal(decodedSessionCookie); }); }); From f03333e2ce35c1ec577c13b32fb3c5983b372c5e Mon Sep 17 00:00:00 2001 From: Xin Li Date: Tue, 10 Aug 2021 12:25:33 -0700 Subject: [PATCH 03/13] remove currentUser.reload --- src/auth/auth.ts | 1 - test/integration/auth.spec.ts | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 477ccd6e7b..5e5431d0f0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -745,7 +745,6 @@ export class BaseAuth implements BaseAuthI // Get tokens valid after time for the corresponding user. return this.getUser(decodedIdToken.sub) .then((user: UserRecord) => { - // If user disabled, throw an auth/user-disabled error. if (user.disabled) { throw new FirebaseAuthError( AuthClientErrorCode.USER_DISABLED, diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index ef5915b3d2..585217b35c 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -879,12 +879,10 @@ describe('admin.auth', () => { it('verifyIdToken() fails with checkRevoked set to true and corresponding user disabled', () => { let currentIdToken: string; - let currentUser: User; return clientAuth().signInWithEmailAndPassword(email, password) .then(({ user }) => { expect(user).to.exist; expect(user!.email).to.equal(email); - currentUser = user!; return user!.getIdToken(); }) .then((idToken) => { @@ -903,11 +901,6 @@ describe('admin.auth', () => { expect(userRecord.disabled).to.equal(true); return admin.auth().verifyIdToken(currentIdToken, false); }) - .then(() => { - // Verification should be successful. - // Need to reload user to ensure currentUser been updated. - return currentUser.reload(); - }) .then(() => { return admin.auth().verifyIdToken(currentIdToken, true); }) @@ -2068,14 +2061,12 @@ describe('admin.auth', () => { const expiresIn = 24 * 60 * 60 * 1000; let currentIdToken: string; let currentSessioncookie: string; - let currentUser: User; return admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }) .then((customToken) => { return clientAuth().signInWithCustomToken(customToken); }) .then(({ user }) => { expect(user).to.exist; - currentUser = user!; return user!.getIdToken(); }) .then((idToken) => { @@ -2098,10 +2089,6 @@ describe('admin.auth', () => { expect(userRecord.uid).to.equal(uid); expect(userRecord.disabled).to.equal(true); return admin.auth().verifySessionCookie(currentSessioncookie, false); - }) - .then(() => { - // Need to reload user to ensure currentUser been updated. - return currentUser.reload(); }).then(() => { admin.auth().verifySessionCookie(currentSessioncookie, true); }) From b03bcffc9605851b4d09c942236a6839135bb04f Mon Sep 17 00:00:00 2001 From: Xin Li Date: Tue, 10 Aug 2021 12:55:16 -0700 Subject: [PATCH 04/13] add return --- 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 585217b35c..d3767ce701 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -2090,7 +2090,7 @@ describe('admin.auth', () => { expect(userRecord.disabled).to.equal(true); return admin.auth().verifySessionCookie(currentSessioncookie, false); }).then(() => { - admin.auth().verifySessionCookie(currentSessioncookie, true); + return admin.auth().verifySessionCookie(currentSessioncookie, true); }) .then(() => { throw new Error('Unexpected success'); From 40a6ab67d16e2cae409f0eaf36d67a6bc25bcb99 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Thu, 12 Aug 2021 17:06:17 -0700 Subject: [PATCH 05/13] Tweak tests --- src/auth/auth.ts | 41 ++++++++++---------- test/integration/auth.spec.ts | 14 +++++-- test/unit/auth/auth.spec.ts | 70 +++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 25 deletions(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 5e5431d0f0..f9b8ffea4f 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -104,19 +104,20 @@ export class BaseAuth implements BaseAuthI } /** - * 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, first verifies whether the corresponding userinfo.disabled - * is true: + * 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, first verifies whether the corresponding + * user is disabled. * If yes, an auth/user-disabled error is thrown. * If no, verifies if the session corresponding to the ID token was revoked. - * If the corresponding user's session was invalidated, an auth/id-token-revoked error is thrown. + * If the corresponding user's session was invalidated, an + * auth/id-token-revoked error is thrown. * If not specified the check is not applied. * * @param {string} idToken The JWT to verify. * @param {boolean=} checkRevoked Whether to check if the ID token is revoked. - * @return {Promise} A Promise that will be fulfilled after a successful - * verification. + * @return {Promise} A Promise that will be fulfilled after + * a successful verification. */ public verifyIdToken(idToken: string, checkRevoked = false): Promise { const isEmulator = useEmulator(); @@ -509,20 +510,22 @@ export class BaseAuth implements BaseAuthI } /** - * Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects - * the promise if the cookie could not be verified. - * If checkRevoked is set to true, first verifies whether the corresponding userinfo.disabled - * is true: + * Verifies a Firebase session cookie. Returns a Promise with the tokens claims. + * Rejects the promise if the cookie could not be verified. + * If checkRevoked is set to true, first verifies whether the corresponding + * user is true: * If yes, an auth/user-disabled error is thrown. - * If no, verifies if the session corresponding to the session cookie was revoked. - * If the corresponding user's session was invalidated, an auth/session-cookie-revoked error - * is thrown. + * If no, verifies if the session corresponding to the session cookie was + * revoked. + * If the corresponding user's session was invalidated, an + * auth/session-cookie-revoked error is thrown. * If not specified the check is not performed. * * @param {string} sessionCookie The session cookie to verify. - * @param {boolean=} checkRevoked Whether to check if the session cookie is revoked. - * @return {Promise} A Promise that will be fulfilled after a successful - * verification. + * @param {boolean=} checkRevoked Whether to check if the session cookie is + * revoked. + * @return {Promise} A Promise that will be fulfilled after + * a successful verification. */ public verifySessionCookie( sessionCookie: string, checkRevoked = false): Promise { @@ -530,7 +533,7 @@ export class BaseAuth implements BaseAuthI return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the cookie was revoked. - if (checkRevoked || isEmulator) { + if (checkRevoked || isEmulator) { return this.verifyDecodedJWTNotRevokedOrDisabled( decodedIdToken, AuthClientErrorCode.SESSION_COOKIE_REVOKED); @@ -748,7 +751,7 @@ export class BaseAuth implements BaseAuthI if (user.disabled) { throw new FirebaseAuthError( AuthClientErrorCode.USER_DISABLED, - 'The user account has been disabled by an administrator'); + 'The user record is disabled.'); } // If no tokens valid after time available, token is not revoked. if (user.tokensValidAfterTime) { diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index d3767ce701..5fdf49b1ed 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -890,6 +890,7 @@ describe('admin.auth', () => { return admin.auth().verifyIdToken(currentIdToken, true); }) .then((decodedIdToken) => { + expect(decodedIdToken.sub).to.equal(uid); expect(decodedIdToken.uid).to.equal(uid); expect(decodedIdToken.email).to.equal(email); return admin.auth().updateUser(uid, { disabled: true }); @@ -901,7 +902,10 @@ describe('admin.auth', () => { expect(userRecord.disabled).to.equal(true); return admin.auth().verifyIdToken(currentIdToken, false); }) - .then(() => { + .then((decodedIdToken) => { + expect(decodedIdToken.sub).to.equal(uid); + expect(decodedIdToken.uid).to.equal(uid); + expect(decodedIdToken.email).to.equal(email); return admin.auth().verifyIdToken(currentIdToken, true); }) .then(() => { @@ -2081,7 +2085,9 @@ describe('admin.auth', () => { currentSessioncookie = sessionCookie; return admin.auth().verifySessionCookie(sessionCookie, true); }) - .then(() => { + .then((decodedIdToken) => { + expect(decodedIdToken.sub).to.equal(uid); + expect(decodedIdToken.uid).to.equal(uid); return admin.auth().updateUser(uid, { disabled : true }); }) .then((userRecord) => { @@ -2089,7 +2095,9 @@ describe('admin.auth', () => { expect(userRecord.uid).to.equal(uid); expect(userRecord.disabled).to.equal(true); return admin.auth().verifySessionCookie(currentSessioncookie, false); - }).then(() => { + }).then((decodedIdToken) => { + expect(decodedIdToken.sub).to.equal(uid); + expect(decodedIdToken.uid).to.equal(uid); return admin.auth().verifySessionCookie(currentSessioncookie, true); }) .then(() => { diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 795ff8926a..77310bf90c 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -865,7 +865,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .resolves(expectedUserRecordDisabled); expect(expectedUserRecordDisabled.disabled).to.be.equal(true); stubs.push(getUserStub); - return auth.verifyIdToken(mockSessionCookie, true) + return auth.verifySessionCookie(mockSessionCookie, true) .then(() => { throw new Error('Unexpected success'); }, (error) => { @@ -3534,9 +3534,9 @@ AUTH_CONFIGS.forEach((testConfig) => { }); describe('auth emulator support', () => { - let mockAuth = testConfig.init(mocks.app()); - const userRecord = getValidUserRecord(getValidGetAccountInfoResponse()); + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(); + const userRecord = getValidUserRecord(expectedAccountInfoResponse); const validSince = new Date(userRecord.tokensValidAfterTime!); const stubs: sinon.SinonStub[] = []; @@ -3595,6 +3595,38 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + it('verifyIdToken() should reject user disabled before ID tokens revoked', () => { + // One second before validSince. + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); + expectedAccountInfoResponseUserDisabled.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + subject: expectedUserRecordDisabled.uid, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase + }); + + // verifyIdToken should force checking revocation in emulator mode, + // even if checkRevoked=false. + return mockAuth.verifyIdToken(unsignedToken, false) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); + }); + }); + it('verifySessionCookie() should reject revoked session cookies', () => { const uid = userRecord.uid; // One second before validSince. @@ -3625,6 +3657,38 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + + it('verifySessionCookie() should reject user disabled before ID tokens revoked', () => { + // One second before validSince. + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); + expectedAccountInfoResponseUserDisabled.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + subject: expectedUserRecordDisabled.uid, + issuer: 'https://session.firebase.google.com/' + mocks.projectId, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase + }); + + return auth.verifySessionCookie(unsignedToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); + it('verifyIdToken() rejects an unsigned token if auth emulator is unreachable', async () => { const unsignedToken = mocks.generateIdToken({ algorithm: 'none' From 72f20d56b3b7adff7730fa2ba9e6be775ee8c069 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Thu, 12 Aug 2021 17:39:24 -0700 Subject: [PATCH 06/13] small fix --- test/unit/auth/auth.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 77310bf90c..cef455b538 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -3597,10 +3597,11 @@ AUTH_CONFIGS.forEach((testConfig) => { it('verifyIdToken() should reject user disabled before ID tokens revoked', () => { // One second before validSince. - const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); expectedAccountInfoResponseUserDisabled.users[0].disabled = true; const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') .resolves(expectedUserRecordDisabled); expect(expectedUserRecordDisabled.disabled).to.be.equal(true); @@ -3660,10 +3661,11 @@ AUTH_CONFIGS.forEach((testConfig) => { it('verifySessionCookie() should reject user disabled before ID tokens revoked', () => { // One second before validSince. - const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); expectedAccountInfoResponseUserDisabled.users[0].disabled = true; const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') .resolves(expectedUserRecordDisabled); expect(expectedUserRecordDisabled.disabled).to.be.equal(true); From b85651fc8a052708e487d879d3af8277e6eae4c2 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Fri, 13 Aug 2021 14:55:03 -0700 Subject: [PATCH 07/13] Use async and await instead of chain in integration test and change CI dependency --- .github/workflows/ci.yml | 2 +- src/auth/auth.ts | 26 +++---- test/integration/auth.spec.ts | 126 ++++++++++++-------------------- test/unit/auth/auth.spec.ts | 134 +++++++++++++++++----------------- 4 files changed, 127 insertions(+), 161 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 244d123dd6..e57ab765b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,6 @@ jobs: run: npm run api-extractor - name: Run emulator-based integration tests run: | - npm install -g firebase-tools + npm install -g firebase-tools@9.16.0 firebase emulators:exec --project fake-project-id --only auth,database,firestore \ 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' diff --git a/src/auth/auth.ts b/src/auth/auth.ts index f9b8ffea4f..8ca6e549c6 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -104,9 +104,9 @@ export class BaseAuth implements BaseAuthI } /** - * 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, first verifies whether the corresponding + * Verifies a JWT auth token. Returns a promise with the tokens claims. + * Rejects the promise if the token cannot be verified. + * If `checkRevoked` is set to true, first verifies whether the corresponding * user is disabled. * If yes, an auth/user-disabled error is thrown. * If no, verifies if the session corresponding to the ID token was revoked. @@ -116,7 +116,7 @@ export class BaseAuth implements BaseAuthI * * @param {string} idToken The JWT to verify. * @param {boolean=} checkRevoked Whether to check if the ID token is revoked. - * @return {Promise} A Promise that will be fulfilled after + * @return {Promise} A promise that will be fulfilled after * a successful verification. */ public verifyIdToken(idToken: string, checkRevoked = false): Promise { @@ -510,10 +510,10 @@ export class BaseAuth implements BaseAuthI } /** - * Verifies a Firebase session cookie. Returns a Promise with the tokens claims. + * Verifies a Firebase session cookie. Returns a promise with the tokens claims. * Rejects the promise if the cookie could not be verified. - * If checkRevoked is set to true, first verifies whether the corresponding - * user is true: + * If `checkRevoked` is set to true, first verifies whether the corresponding + * user is disabled: * If yes, an auth/user-disabled error is thrown. * If no, verifies if the session corresponding to the session cookie was * revoked. @@ -524,7 +524,7 @@ export class BaseAuth implements BaseAuthI * @param {string} sessionCookie The session cookie to verify. * @param {boolean=} checkRevoked Whether to check if the session cookie is * revoked. - * @return {Promise} A Promise that will be fulfilled after + * @return {Promise} A promise that will be fulfilled after * a successful verification. */ public verifySessionCookie( @@ -740,7 +740,7 @@ export class BaseAuth implements BaseAuthI * @param {DecodedIdToken} decodedIdToken The JWT's decoded claims. * @param {ErrorInfo} revocationErrorInfo The revocation error info to throw on revocation * detection. - * @return {Promise} A Promise that will be fulfilled after a successful + * @return {Promise} A promise that will be fulfilled after a successful * verification. */ private verifyDecodedJWTNotRevokedOrDisabled( @@ -794,7 +794,7 @@ export class TenantAwareAuth } /** - * Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects + * 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, * verifies if the session corresponding to the ID token was revoked. If the corresponding * user's session was invalidated, an auth/id-token-revoked error is thrown. If not specified @@ -802,7 +802,7 @@ export class TenantAwareAuth * * @param {string} idToken The JWT to verify. * @param {boolean=} checkRevoked Whether to check if the ID token is revoked. - * @return {Promise} A Promise that will be fulfilled after a successful + * @return {Promise} A promise that will be fulfilled after a successful * verification. */ public verifyIdToken(idToken: string, checkRevoked = false): Promise { @@ -845,7 +845,7 @@ export class TenantAwareAuth } /** - * Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects + * Verifies a Firebase session cookie. Returns a promise with the tokens claims. Rejects * the promise if the token could not be verified. If checkRevoked is set to true, * verifies if the session corresponding to the session cookie was revoked. If the corresponding * user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not @@ -853,7 +853,7 @@ export class TenantAwareAuth * * @param {string} sessionCookie The session cookie to verify. * @param {boolean=} checkRevoked Whether to check if the session cookie is revoked. - * @return {Promise} A Promise that will be fulfilled after a successful + * @return {Promise} A promise that will be fulfilled after a successful * verification. */ public verifySessionCookie( diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 5fdf49b1ed..e6792751c9 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -877,42 +877,28 @@ describe('admin.auth', () => { .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); - it('verifyIdToken() fails with checkRevoked set to true and corresponding user disabled', () => { - let currentIdToken: string; - return clientAuth().signInWithEmailAndPassword(email, password) - .then(({ user }) => { - expect(user).to.exist; - expect(user!.email).to.equal(email); - return user!.getIdToken(); - }) - .then((idToken) => { - currentIdToken = idToken; - return admin.auth().verifyIdToken(currentIdToken, true); - }) - .then((decodedIdToken) => { - expect(decodedIdToken.sub).to.equal(uid); - expect(decodedIdToken.uid).to.equal(uid); - expect(decodedIdToken.email).to.equal(email); - return admin.auth().updateUser(uid, { disabled: true }); - }) - .then((userRecord) => { - // Ensure disabled field has been updated. - expect(userRecord.uid).to.equal(uid); - expect(userRecord.email).to.equal(email); - expect(userRecord.disabled).to.equal(true); - return admin.auth().verifyIdToken(currentIdToken, false); - }) - .then((decodedIdToken) => { - expect(decodedIdToken.sub).to.equal(uid); - expect(decodedIdToken.uid).to.equal(uid); - expect(decodedIdToken.email).to.equal(email); - return admin.auth().verifyIdToken(currentIdToken, true); - }) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.have.property('code', 'auth/user-disabled'); - }); + it('verifyIdToken() fails with checkRevoked set to true and corresponding user disabled', async () => { + const { user } = await clientAuth().signInWithEmailAndPassword(email, password); + expect(user).to.exist; + expect(user!.email).to.equal(email); + + const idToken = await user!.getIdToken(); + let decodedIdToken = await admin.auth().verifyIdToken(idToken, true); + expect(decodedIdToken.uid).to.equal(uid); + expect(decodedIdToken.email).to.equal(email); + + const userRecord = await admin.auth().updateUser(uid, { disabled: true }); + expect(userRecord.uid).to.equal(uid); + expect(userRecord.email).to.equal(email); + expect(userRecord.disabled).to.equal(true); + + decodedIdToken = await admin.auth().verifyIdToken(idToken, false); + expect(decodedIdToken.uid).to.equal(uid); + try { + await admin.auth().verifyIdToken(idToken, true); + } catch (error) { + expect(error).to.have.property('code', 'auth/user-disabled'); + } }); if (authEmulatorHost) { @@ -2061,50 +2047,32 @@ describe('admin.auth', () => { }); }); - it('fails with checkRevoked set to true and corresponding user disabled', () => { + it('fails with checkRevoked set to true and corresponding user disabled', async () => { const expiresIn = 24 * 60 * 60 * 1000; - let currentIdToken: string; - let currentSessioncookie: string; - return admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }) - .then((customToken) => { - return clientAuth().signInWithCustomToken(customToken); - }) - .then(({ user }) => { - expect(user).to.exist; - return user!.getIdToken(); - }) - .then((idToken) => { - currentIdToken = idToken; - return admin.auth().verifyIdToken(idToken); - }).then((decodedIdTokenClaims) => { - expect(decodedIdTokenClaims.uid).to.be.equal(uid); - // One day long session cookie. - return admin.auth().createSessionCookie(currentIdToken, { expiresIn }); - }) - .then((sessionCookie) => { - currentSessioncookie = sessionCookie; - return admin.auth().verifySessionCookie(sessionCookie, true); - }) - .then((decodedIdToken) => { - expect(decodedIdToken.sub).to.equal(uid); - expect(decodedIdToken.uid).to.equal(uid); - return admin.auth().updateUser(uid, { disabled : true }); - }) - .then((userRecord) => { - // Ensure disabled field has been updated. - expect(userRecord.uid).to.equal(uid); - expect(userRecord.disabled).to.equal(true); - return admin.auth().verifySessionCookie(currentSessioncookie, false); - }).then((decodedIdToken) => { - expect(decodedIdToken.sub).to.equal(uid); - expect(decodedIdToken.uid).to.equal(uid); - return admin.auth().verifySessionCookie(currentSessioncookie, true); - }) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.have.property('code', 'auth/user-disabled'); - }); + const customToken = await admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }); + const { user } = await clientAuth().signInWithCustomToken(customToken); + expect(user).to.exist; + + const idToken = await user!.getIdToken(); + const decodedIdTokenClaims = await admin.auth().verifyIdToken(idToken); + expect(decodedIdTokenClaims.uid).to.be.equal(uid); + + const sessionCookie = await admin.auth().createSessionCookie(idToken, { expiresIn }); + let decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, true); + expect(decodedIdToken.uid).to.equal(uid); + + const userRecord = await admin.auth().updateUser(uid, { disabled : true }); + // Ensure disabled field has been updated. + expect(userRecord.uid).to.equal(uid); + expect(userRecord.disabled).to.equal(true); + + decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, false); + expect(decodedIdToken.uid).to.equal(uid); + try { + admin.auth().verifySessionCookie(sessionCookie, true); + } catch (error) { + expect(error).to.have.property('code', 'auth/user-disabled'); + } }); }); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index cef455b538..7c1e895cb9 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -501,6 +501,38 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + it('verifyIdToken() should reject user disabled before ID tokens revoked', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); + expectedAccountInfoResponseUserDisabled.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + // One second before validSince. + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + subject: expectedUserRecordDisabled.uid, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase + }); + + return auth.verifyIdToken(unsignedToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); + }); + }); + it('should work with a non-cert credential when the GOOGLE_CLOUD_PROJECT environment variable is present', () => { process.env.GOOGLE_CLOUD_PROJECT = mocks.projectId; @@ -876,6 +908,39 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + it('verifySessionCookie() should reject user disabled before ID tokens revoked', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); + expectedAccountInfoResponseUserDisabled.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + // One second before validSince. + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + subject: expectedUserRecordDisabled.uid, + issuer: 'https://session.firebase.google.com/' + mocks.projectId, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase + }); + + return auth.verifySessionCookie(unsignedToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); + it('should be fulfilled with checkRevoked set to true when no validSince available', () => { // Simulate no validSince set on the user. const noValidSinceGetAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); @@ -3535,8 +3600,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('auth emulator support', () => { let mockAuth = testConfig.init(mocks.app()); - const expectedAccountInfoResponse = getValidGetAccountInfoResponse(); - const userRecord = getValidUserRecord(expectedAccountInfoResponse); + const userRecord = getValidUserRecord(getValidGetAccountInfoResponse()); const validSince = new Date(userRecord.tokensValidAfterTime!); const stubs: sinon.SinonStub[] = []; @@ -3595,39 +3659,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); - it('verifyIdToken() should reject user disabled before ID tokens revoked', () => { - // One second before validSince. - const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); - expectedAccountInfoResponseUserDisabled.users[0].disabled = true; - const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); - const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); - const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); - const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') - .resolves(expectedUserRecordDisabled); - expect(expectedUserRecordDisabled.disabled).to.be.equal(true); - stubs.push(getUserStub); - - const unsignedToken = mocks.generateIdToken({ - algorithm: 'none', - subject: expectedUserRecordDisabled.uid, - }, { - iat: oneSecBeforeValidSince, - auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase - }); - - // verifyIdToken should force checking revocation in emulator mode, - // even if checkRevoked=false. - return mockAuth.verifyIdToken(unsignedToken, false) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm expected error returned. - expect(error).to.have.property('code', 'auth/user-disabled'); - // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); - }); - }); - it('verifySessionCookie() should reject revoked session cookies', () => { const uid = userRecord.uid; // One second before validSince. @@ -3658,39 +3689,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); - - it('verifySessionCookie() should reject user disabled before ID tokens revoked', () => { - // One second before validSince. - const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); - expectedAccountInfoResponseUserDisabled.users[0].disabled = true; - const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); - const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); - const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); - const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') - .resolves(expectedUserRecordDisabled); - expect(expectedUserRecordDisabled.disabled).to.be.equal(true); - stubs.push(getUserStub); - - const unsignedToken = mocks.generateIdToken({ - algorithm: 'none', - subject: expectedUserRecordDisabled.uid, - issuer: 'https://session.firebase.google.com/' + mocks.projectId, - }, { - iat: oneSecBeforeValidSince, - auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase - }); - - return auth.verifySessionCookie(unsignedToken, true) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); - // Confirm expected error returned. - expect(error).to.have.property('code', 'auth/user-disabled'); - }); - }); - it('verifyIdToken() rejects an unsigned token if auth emulator is unreachable', async () => { const unsignedToken = mocks.generateIdToken({ algorithm: 'none' From a8ee801a7556bafa2710a0ee804fb8dc0da5fced Mon Sep 17 00:00:00 2001 From: Xin Li Date: Fri, 13 Aug 2021 16:17:38 -0700 Subject: [PATCH 08/13] retry --- 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 e6792751c9..966ebaafd3 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -2069,7 +2069,7 @@ describe('admin.auth', () => { decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, false); expect(decodedIdToken.uid).to.equal(uid); try { - admin.auth().verifySessionCookie(sessionCookie, true); + await admin.auth().verifySessionCookie(sessionCookie, true); } catch (error) { expect(error).to.have.property('code', 'auth/user-disabled'); } From c5085238a2572ac0eb9c99b68af9058ae85aefb7 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Fri, 13 Aug 2021 18:17:02 -0700 Subject: [PATCH 09/13] Add special case of authEmulator --- test/integration/auth.spec.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 966ebaafd3..744ea4932c 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -892,8 +892,17 @@ describe('admin.auth', () => { expect(userRecord.email).to.equal(email); expect(userRecord.disabled).to.equal(true); - decodedIdToken = await admin.auth().verifyIdToken(idToken, false); - expect(decodedIdToken.uid).to.equal(uid); + try { + // If it is in emulator mode, a user-disabled error will be thrown. + decodedIdToken = await admin.auth().verifyIdToken(idToken, false); + expect(decodedIdToken.uid).to.equal(uid); + } catch (error) { + if (authEmulatorHost) { + expect(error).to.have.property('code', 'auth/user-disabled'); + } + throw new Error(error); + } + try { await admin.auth().verifyIdToken(idToken, true); } catch (error) { @@ -2065,9 +2074,18 @@ describe('admin.auth', () => { // Ensure disabled field has been updated. expect(userRecord.uid).to.equal(uid); expect(userRecord.disabled).to.equal(true); + + try { + // If it is in emulator mode, a user-disabled error will be thrown. + decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, false); + expect(decodedIdToken.uid).to.equal(uid); + } catch (error) { + if (authEmulatorHost) { + expect(error).to.have.property('code', 'auth/user-disabled'); + } + throw new Error(error); + } - decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, false); - expect(decodedIdToken.uid).to.equal(uid); try { await admin.auth().verifySessionCookie(sessionCookie, true); } catch (error) { From 6175c59ecc02d21adafe13c1cfeccc8c2974fa5f Mon Sep 17 00:00:00 2001 From: Xin Li Date: Fri, 13 Aug 2021 18:28:01 -0700 Subject: [PATCH 10/13] fix emulator on issue --- test/integration/auth.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 744ea4932c..7d275a5f5d 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -899,8 +899,9 @@ describe('admin.auth', () => { } catch (error) { if (authEmulatorHost) { expect(error).to.have.property('code', 'auth/user-disabled'); + } else { + throw error; } - throw new Error(error); } try { @@ -2078,12 +2079,13 @@ describe('admin.auth', () => { try { // If it is in emulator mode, a user-disabled error will be thrown. decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, false); - expect(decodedIdToken.uid).to.equal(uid); + expect(decodedIdToken.uid).to.equal(uid); } catch (error) { if (authEmulatorHost) { expect(error).to.have.property('code', 'auth/user-disabled'); + } else { + throw error; } - throw new Error(error); } try { From 1a687fb21f0c6b33f18a957e8815bf2c89b0e3ae Mon Sep 17 00:00:00 2001 From: Xin Li Date: Mon, 16 Aug 2021 12:26:21 -0700 Subject: [PATCH 11/13] remove firebase-tool version changes --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e57ab765b6..244d123dd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,6 @@ jobs: run: npm run api-extractor - name: Run emulator-based integration tests run: | - npm install -g firebase-tools@9.16.0 + npm install -g firebase-tools firebase emulators:exec --project fake-project-id --only auth,database,firestore \ 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' From 43e36643dc157814d74ef917207ff72ea846f5e5 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Mon, 16 Aug 2021 15:14:45 -0700 Subject: [PATCH 12/13] useMockIdToken for unit test --- test/unit/auth/auth.spec.ts | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 7c1e895cb9..73481dd76e 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -507,22 +507,18 @@ AUTH_CONFIGS.forEach((testConfig) => { expectedAccountInfoResponseUserDisabled.users[0].disabled = true; const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + // Restore verifyIdToken stub. + stub.restore(); // One second before validSince. - const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(getDecodedIdToken(expectedUserRecordDisabled.uid, oneSecBeforeValidSince)); + stubs.push(stub); const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') .resolves(expectedUserRecordDisabled); expect(expectedUserRecordDisabled.disabled).to.be.equal(true); stubs.push(getUserStub); - - const unsignedToken = mocks.generateIdToken({ - algorithm: 'none', - subject: expectedUserRecordDisabled.uid, - }, { - iat: oneSecBeforeValidSince, - auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase - }); - - return auth.verifyIdToken(unsignedToken, true) + return auth.verifyIdToken(mockIdToken, true) .then(() => { throw new Error('Unexpected success'); }, (error) => { @@ -914,23 +910,18 @@ AUTH_CONFIGS.forEach((testConfig) => { expectedAccountInfoResponseUserDisabled.users[0].disabled = true; const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + // Restore verifyIdToken stub. + stub.restore(); // One second before validSince. - const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(getDecodedIdToken(expectedUserRecordDisabled.uid, oneSecBeforeValidSince)); + stubs.push(stub); const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') .resolves(expectedUserRecordDisabled); expect(expectedUserRecordDisabled.disabled).to.be.equal(true); stubs.push(getUserStub); - - const unsignedToken = mocks.generateIdToken({ - algorithm: 'none', - subject: expectedUserRecordDisabled.uid, - issuer: 'https://session.firebase.google.com/' + mocks.projectId, - }, { - iat: oneSecBeforeValidSince, - auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase - }); - - return auth.verifySessionCookie(unsignedToken, true) + return auth.verifySessionCookie(mockSessionCookie, true) .then(() => { throw new Error('Unexpected success'); }, (error) => { From 50c33d840d774eea96a5340e1ae4a1015ab3eea4 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Mon, 16 Aug 2021 16:37:25 -0700 Subject: [PATCH 13/13] fix typos --- 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 73481dd76e..836a7f29ba 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -910,7 +910,7 @@ AUTH_CONFIGS.forEach((testConfig) => { expectedAccountInfoResponseUserDisabled.users[0].disabled = true; const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); - // Restore verifyIdToken stub. + // Restore verifySessionCookie stub. stub.restore(); // One second before validSince. const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000);