Skip to content

Commit 9f7529f

Browse files
authored
fix: Throw error on user disabled and check revoked set true (#1401)
* fix: Throw error on user disabled and check revoked set true * resolve 2 calls on getUser(), improve tests and lints * remove currentUser.reload * add return * Tweak tests * small fix * Use async and await instead of chain in integration test and change CI dependency * retry * Add special case of authEmulator * fix emulator on issue * remove firebase-tool version changes * useMockIdToken for unit test * fix typos
1 parent e22f0ef commit 9f7529f

File tree

4 files changed

+291
-66
lines changed

4 files changed

+291
-66
lines changed

src/auth/auth.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -104,24 +104,28 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
104104
}
105105

106106
/**
107-
* Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects
108-
* the promise if the token could not be verified. If checkRevoked is set to true,
109-
* verifies if the session corresponding to the ID token was revoked. If the corresponding
110-
* user's session was invalidated, an auth/id-token-revoked error is thrown. If not specified
111-
* the check is not applied.
107+
* Verifies a JWT auth token. Returns a promise with the tokens claims.
108+
* Rejects the promise if the token cannot be verified.
109+
* If `checkRevoked` is set to true, first verifies whether the corresponding
110+
* user is disabled.
111+
* If yes, an auth/user-disabled error is thrown.
112+
* If no, verifies if the session corresponding to the ID token was revoked.
113+
* If the corresponding user's session was invalidated, an
114+
* auth/id-token-revoked error is thrown.
115+
* If not specified the check is not applied.
112116
*
113117
* @param {string} idToken The JWT to verify.
114118
* @param {boolean=} checkRevoked Whether to check if the ID token is revoked.
115-
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
116-
* verification.
119+
* @return {Promise<DecodedIdToken>} A promise that will be fulfilled after
120+
* a successful verification.
117121
*/
118122
public verifyIdToken(idToken: string, checkRevoked = false): Promise<DecodedIdToken> {
119123
const isEmulator = useEmulator();
120124
return this.idTokenVerifier.verifyJWT(idToken, isEmulator)
121125
.then((decodedIdToken: DecodedIdToken) => {
122126
// Whether to check if the token was revoked.
123127
if (checkRevoked || isEmulator) {
124-
return this.verifyDecodedJWTNotRevoked(
128+
return this.verifyDecodedJWTNotRevokedOrDisabled(
125129
decodedIdToken,
126130
AuthClientErrorCode.ID_TOKEN_REVOKED);
127131
}
@@ -506,25 +510,31 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
506510
}
507511

508512
/**
509-
* Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects
510-
* the promise if the token could not be verified. If checkRevoked is set to true,
511-
* verifies if the session corresponding to the session cookie was revoked. If the corresponding
512-
* user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not
513-
* specified the check is not performed.
513+
* Verifies a Firebase session cookie. Returns a promise with the tokens claims.
514+
* Rejects the promise if the cookie could not be verified.
515+
* If `checkRevoked` is set to true, first verifies whether the corresponding
516+
* user is disabled:
517+
* If yes, an auth/user-disabled error is thrown.
518+
* If no, verifies if the session corresponding to the session cookie was
519+
* revoked.
520+
* If the corresponding user's session was invalidated, an
521+
* auth/session-cookie-revoked error is thrown.
522+
* If not specified the check is not performed.
514523
*
515524
* @param {string} sessionCookie The session cookie to verify.
516-
* @param {boolean=} checkRevoked Whether to check if the session cookie is revoked.
517-
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
518-
* verification.
525+
* @param {boolean=} checkRevoked Whether to check if the session cookie is
526+
* revoked.
527+
* @return {Promise<DecodedIdToken>} A promise that will be fulfilled after
528+
* a successful verification.
519529
*/
520530
public verifySessionCookie(
521531
sessionCookie: string, checkRevoked = false): Promise<DecodedIdToken> {
522532
const isEmulator = useEmulator();
523533
return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator)
524534
.then((decodedIdToken: DecodedIdToken) => {
525-
// Whether to check if the token was revoked.
535+
// Whether to check if the cookie was revoked.
526536
if (checkRevoked || isEmulator) {
527-
return this.verifyDecodedJWTNotRevoked(
537+
return this.verifyDecodedJWTNotRevokedOrDisabled(
528538
decodedIdToken,
529539
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
530540
}
@@ -723,20 +733,26 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
723733
}
724734

725735
/**
726-
* Verifies the decoded Firebase issued JWT is not revoked. Returns a promise that resolves
727-
* with the decoded claims on success. Rejects the promise with revocation error if revoked.
736+
* Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that
737+
* resolves with the decoded claims on success. Rejects the promise with revocation error if revoked
738+
* or user disabled.
728739
*
729740
* @param {DecodedIdToken} decodedIdToken The JWT's decoded claims.
730741
* @param {ErrorInfo} revocationErrorInfo The revocation error info to throw on revocation
731742
* detection.
732-
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
743+
* @return {Promise<DecodedIdToken>} A promise that will be fulfilled after a successful
733744
* verification.
734745
*/
735-
private verifyDecodedJWTNotRevoked(
746+
private verifyDecodedJWTNotRevokedOrDisabled(
736747
decodedIdToken: DecodedIdToken, revocationErrorInfo: ErrorInfo): Promise<DecodedIdToken> {
737748
// Get tokens valid after time for the corresponding user.
738749
return this.getUser(decodedIdToken.sub)
739750
.then((user: UserRecord) => {
751+
if (user.disabled) {
752+
throw new FirebaseAuthError(
753+
AuthClientErrorCode.USER_DISABLED,
754+
'The user record is disabled.');
755+
}
740756
// If no tokens valid after time available, token is not revoked.
741757
if (user.tokensValidAfterTime) {
742758
// Get the ID token authentication time and convert to milliseconds UTC.
@@ -778,15 +794,15 @@ export class TenantAwareAuth
778794
}
779795

780796
/**
781-
* Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects
797+
* Verifies a JWT auth token. Returns a promise with the tokens claims. Rejects
782798
* the promise if the token could not be verified. If checkRevoked is set to true,
783799
* verifies if the session corresponding to the ID token was revoked. If the corresponding
784800
* user's session was invalidated, an auth/id-token-revoked error is thrown. If not specified
785801
* the check is not applied.
786802
*
787803
* @param {string} idToken The JWT to verify.
788804
* @param {boolean=} checkRevoked Whether to check if the ID token is revoked.
789-
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
805+
* @return {Promise<DecodedIdToken>} A promise that will be fulfilled after a successful
790806
* verification.
791807
*/
792808
public verifyIdToken(idToken: string, checkRevoked = false): Promise<DecodedIdToken> {
@@ -829,15 +845,15 @@ export class TenantAwareAuth
829845
}
830846

831847
/**
832-
* Verifies a Firebase session cookie. Returns a Promise with the tokens claims. Rejects
848+
* Verifies a Firebase session cookie. Returns a promise with the tokens claims. Rejects
833849
* the promise if the token could not be verified. If checkRevoked is set to true,
834850
* verifies if the session corresponding to the session cookie was revoked. If the corresponding
835851
* user's session was invalidated, an auth/session-cookie-revoked error is thrown. If not
836852
* specified the check is not performed.
837853
*
838854
* @param {string} sessionCookie The session cookie to verify.
839855
* @param {boolean=} checkRevoked Whether to check if the session cookie is revoked.
840-
* @return {Promise<DecodedIdToken>} A Promise that will be fulfilled after a successful
856+
* @return {Promise<DecodedIdToken>} A promise that will be fulfilled after a successful
841857
* verification.
842858
*/
843859
public verifySessionCookie(

src/utils/error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,10 @@ export class AuthClientErrorCode {
721721
code: 'not-found',
722722
message: 'The requested resource was not found.',
723723
};
724+
public static USER_DISABLED = {
725+
code: 'user-disabled',
726+
message: 'The user record is disabled.',
727+
}
724728
public static USER_NOT_DISABLED = {
725729
code: 'user-not-disabled',
726730
message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).',

test/integration/auth.spec.ts

Lines changed: 151 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,20 @@ describe('admin.auth', () => {
760760
safeDelete(userRecord.uid);
761761
}
762762
});
763+
764+
it('A user with user record disabled is unable to sign in', async () => {
765+
const password = 'password';
766+
const email = '[email protected]';
767+
return admin.auth().updateUser(updateUser.uid, { disabled : true , password, email })
768+
.then(() => {
769+
return clientAuth().signInWithEmailAndPassword(email, password);
770+
})
771+
.then(() => {
772+
throw new Error('Unexpected success');
773+
}, (error) => {
774+
expect(error).to.have.property('code', 'auth/user-disabled');
775+
});
776+
});
763777
});
764778

765779
it('getUser() fails when called with a non-existing UID', () => {
@@ -833,53 +847,113 @@ describe('admin.auth', () => {
833847
});
834848
});
835849

836-
it('verifyIdToken() fails when called with an invalid token', () => {
837-
return admin.auth().verifyIdToken('invalid-token')
838-
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
839-
});
850+
describe('verifyIdToken()', () => {
851+
const uid = generateRandomString(20).toLowerCase();
852+
const email = uid + '@example.com';
853+
const password = 'password';
854+
const userData = {
855+
uid,
856+
email,
857+
emailVerified: false,
858+
password,
859+
};
860+
861+
// Create the test user before running this suite of tests.
862+
before(() => {
863+
return admin.auth().createUser(userData);
864+
});
840865

841-
if (authEmulatorHost) {
842-
describe('Auth emulator support', () => {
843-
const uid = 'authEmulatorUser';
844-
before(() => {
845-
return admin.auth().createUser({
846-
uid,
847-
848-
password: 'p4ssword',
866+
// Sign out after each test.
867+
afterEach(() => {
868+
return clientAuth().signOut();
869+
});
870+
871+
after(() => {
872+
return safeDelete(uid);
873+
});
874+
875+
it('verifyIdToken() fails when called with an invalid token', () => {
876+
return admin.auth().verifyIdToken('invalid-token')
877+
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
878+
});
879+
880+
it('verifyIdToken() fails with checkRevoked set to true and corresponding user disabled', async () => {
881+
const { user } = await clientAuth().signInWithEmailAndPassword(email, password);
882+
expect(user).to.exist;
883+
expect(user!.email).to.equal(email);
884+
885+
const idToken = await user!.getIdToken();
886+
let decodedIdToken = await admin.auth().verifyIdToken(idToken, true);
887+
expect(decodedIdToken.uid).to.equal(uid);
888+
expect(decodedIdToken.email).to.equal(email);
889+
890+
const userRecord = await admin.auth().updateUser(uid, { disabled: true });
891+
expect(userRecord.uid).to.equal(uid);
892+
expect(userRecord.email).to.equal(email);
893+
expect(userRecord.disabled).to.equal(true);
894+
895+
try {
896+
// If it is in emulator mode, a user-disabled error will be thrown.
897+
decodedIdToken = await admin.auth().verifyIdToken(idToken, false);
898+
expect(decodedIdToken.uid).to.equal(uid);
899+
} catch (error) {
900+
if (authEmulatorHost) {
901+
expect(error).to.have.property('code', 'auth/user-disabled');
902+
} else {
903+
throw error;
904+
}
905+
}
906+
907+
try {
908+
await admin.auth().verifyIdToken(idToken, true);
909+
} catch (error) {
910+
expect(error).to.have.property('code', 'auth/user-disabled');
911+
}
912+
});
913+
914+
if (authEmulatorHost) {
915+
describe('Auth emulator support', () => {
916+
const uid = 'authEmulatorUser';
917+
before(() => {
918+
return admin.auth().createUser({
919+
uid,
920+
921+
password: 'p4ssword',
922+
});
923+
});
924+
after(() => {
925+
return admin.auth().deleteUser(uid);
849926
});
850-
});
851-
after(() => {
852-
return admin.auth().deleteUser(uid);
853-
});
854927

855-
it('verifyIdToken() succeeds when called with an unsigned token', () => {
856-
const unsignedToken = mocks.generateIdToken({
857-
algorithm: 'none',
858-
audience: projectId,
859-
issuer: 'https://securetoken.google.com/' + projectId,
860-
subject: uid,
928+
it('verifyIdToken() succeeds when called with an unsigned token', () => {
929+
const unsignedToken = mocks.generateIdToken({
930+
algorithm: 'none',
931+
audience: projectId,
932+
issuer: 'https://securetoken.google.com/' + projectId,
933+
subject: uid,
934+
});
935+
return admin.auth().verifyIdToken(unsignedToken);
861936
});
862-
return admin.auth().verifyIdToken(unsignedToken);
863-
});
864937

865-
it('verifyIdToken() fails when called with a token with wrong project', () => {
866-
const unsignedToken = mocks.generateIdToken({ algorithm: 'none', audience: 'nosuch' });
867-
return admin.auth().verifyIdToken(unsignedToken)
868-
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
869-
});
938+
it('verifyIdToken() fails when called with a token with wrong project', () => {
939+
const unsignedToken = mocks.generateIdToken({ algorithm: 'none', audience: 'nosuch' });
940+
return admin.auth().verifyIdToken(unsignedToken)
941+
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
942+
});
870943

871-
it('verifyIdToken() fails when called with a token that does not belong to a user', () => {
872-
const unsignedToken = mocks.generateIdToken({
873-
algorithm: 'none',
874-
audience: projectId,
875-
issuer: 'https://securetoken.google.com/' + projectId,
876-
subject: 'nosuch',
944+
it('verifyIdToken() fails when called with a token that does not belong to a user', () => {
945+
const unsignedToken = mocks.generateIdToken({
946+
algorithm: 'none',
947+
audience: projectId,
948+
issuer: 'https://securetoken.google.com/' + projectId,
949+
subject: 'nosuch',
950+
});
951+
return admin.auth().verifyIdToken(unsignedToken)
952+
.should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found');
877953
});
878-
return admin.auth().verifyIdToken(unsignedToken)
879-
.should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found');
880954
});
881-
});
882-
}
955+
}
956+
});
883957

884958
describe('Link operations', () => {
885959
const uid = generateRandomString(20).toLowerCase();
@@ -1982,6 +2056,44 @@ describe('admin.auth', () => {
19822056
.should.eventually.be.rejected.and.have.property('code', 'auth/argument-error');
19832057
});
19842058
});
2059+
2060+
it('fails with checkRevoked set to true and corresponding user disabled', async () => {
2061+
const expiresIn = 24 * 60 * 60 * 1000;
2062+
const customToken = await admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' });
2063+
const { user } = await clientAuth().signInWithCustomToken(customToken);
2064+
expect(user).to.exist;
2065+
2066+
const idToken = await user!.getIdToken();
2067+
const decodedIdTokenClaims = await admin.auth().verifyIdToken(idToken);
2068+
expect(decodedIdTokenClaims.uid).to.be.equal(uid);
2069+
2070+
const sessionCookie = await admin.auth().createSessionCookie(idToken, { expiresIn });
2071+
let decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, true);
2072+
expect(decodedIdToken.uid).to.equal(uid);
2073+
2074+
const userRecord = await admin.auth().updateUser(uid, { disabled : true });
2075+
// Ensure disabled field has been updated.
2076+
expect(userRecord.uid).to.equal(uid);
2077+
expect(userRecord.disabled).to.equal(true);
2078+
2079+
try {
2080+
// If it is in emulator mode, a user-disabled error will be thrown.
2081+
decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie, false);
2082+
expect(decodedIdToken.uid).to.equal(uid);
2083+
} catch (error) {
2084+
if (authEmulatorHost) {
2085+
expect(error).to.have.property('code', 'auth/user-disabled');
2086+
} else {
2087+
throw error;
2088+
}
2089+
}
2090+
2091+
try {
2092+
await admin.auth().verifySessionCookie(sessionCookie, true);
2093+
} catch (error) {
2094+
expect(error).to.have.property('code', 'auth/user-disabled');
2095+
}
2096+
});
19852097
});
19862098

19872099
describe('importUsers()', () => {

0 commit comments

Comments
 (0)