Skip to content

Commit 6ce98e2

Browse files
authored
Improve token verification logic with Auth Emulator. (#1148)
* Improve token verification logic with Auth Emulator. * Clean up comments. * Fix linting issues. * Address review comments. * Use mock for auth emulator unit test. * Implement session cookies. * Call useEmulator() only once. * Update tests. * Delete unused test helper. * Add unit tests for checking revocation. * Fix typo in test comments.
1 parent 1862342 commit 6ce98e2

File tree

9 files changed

+321
-173
lines changed

9 files changed

+321
-173
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
},
7070
"devDependencies": {
7171
"@firebase/app": "^0.6.13",
72-
"@firebase/auth": "^0.15.2",
72+
"@firebase/auth": "^0.16.2",
7373
"@firebase/auth-types": "^0.10.1",
7474
"@microsoft/api-extractor": "^7.11.2",
7575
"@types/bcrypt": "^2.0.0",

src/auth/auth-api-request.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,10 +2117,6 @@ function emulatorHost(): string | undefined {
21172117
/**
21182118
* When true the SDK should communicate with the Auth Emulator for all API
21192119
* calls and also produce unsigned tokens.
2120-
*
2121-
* This alone does <b>NOT<b> short-circuit ID Token verification.
2122-
* For security reasons that must be explicitly disabled through
2123-
* setJwtVerificationEnabled(false);
21242120
*/
21252121
export function useEmulator(): boolean {
21262122
return !!emulatorHost();

src/auth/auth.ts

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import * as utils from '../utils/index';
2929
import * as validator from '../utils/validator';
3030
import { auth } from './index';
3131
import {
32-
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256
32+
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier
3333
} from './token-verifier';
3434
import {
3535
SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
@@ -115,15 +115,16 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
115115
* verification.
116116
*/
117117
public verifyIdToken(idToken: string, checkRevoked = false): Promise<DecodedIdToken> {
118-
return this.idTokenVerifier.verifyJWT(idToken)
118+
const isEmulator = useEmulator();
119+
return this.idTokenVerifier.verifyJWT(idToken, isEmulator)
119120
.then((decodedIdToken: DecodedIdToken) => {
120121
// Whether to check if the token was revoked.
121-
if (!checkRevoked) {
122-
return decodedIdToken;
122+
if (checkRevoked || isEmulator) {
123+
return this.verifyDecodedJWTNotRevoked(
124+
decodedIdToken,
125+
AuthClientErrorCode.ID_TOKEN_REVOKED);
123126
}
124-
return this.verifyDecodedJWTNotRevoked(
125-
decodedIdToken,
126-
AuthClientErrorCode.ID_TOKEN_REVOKED);
127+
return decodedIdToken;
127128
});
128129
}
129130

@@ -443,15 +444,16 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
443444
*/
444445
public verifySessionCookie(
445446
sessionCookie: string, checkRevoked = false): Promise<DecodedIdToken> {
446-
return this.sessionCookieVerifier.verifyJWT(sessionCookie)
447+
const isEmulator = useEmulator();
448+
return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator)
447449
.then((decodedIdToken: DecodedIdToken) => {
448450
// Whether to check if the token was revoked.
449-
if (!checkRevoked) {
450-
return decodedIdToken;
451+
if (checkRevoked || isEmulator) {
452+
return this.verifyDecodedJWTNotRevoked(
453+
decodedIdToken,
454+
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
451455
}
452-
return this.verifyDecodedJWTNotRevoked(
453-
decodedIdToken,
454-
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
456+
return decodedIdToken;
455457
});
456458
}
457459

@@ -675,28 +677,6 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
675677
return decodedIdToken;
676678
});
677679
}
678-
679-
/**
680-
* Enable or disable ID token verification. This is used to safely short-circuit token verification with the
681-
* Auth emulator. When disabled ONLY unsigned tokens will pass verification, production tokens will not pass.
682-
*
683-
* WARNING: This is a dangerous method that will compromise your app's security and break your app in
684-
* production. Developers should never call this method, it is for internal testing use only.
685-
*
686-
* @internal
687-
*/
688-
// @ts-expect-error: this method appears unused but is used privately.
689-
private setJwtVerificationEnabled(enabled: boolean): void {
690-
if (!enabled && !useEmulator()) {
691-
// We only allow verification to be disabled in conjunction with
692-
// the emulator environment variable.
693-
throw new Error('This method is only available when connected to the Authentication emulator.');
694-
}
695-
696-
const algorithm = enabled ? ALGORITHM_RS256 : 'none';
697-
this.idTokenVerifier.setAlgorithm(algorithm);
698-
this.sessionCookieVerifier.setAlgorithm(algorithm);
699-
}
700680
}
701681

702682

src/auth/token-verifier.ts

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class FirebaseTokenVerifier {
7979
constructor(private clientCertUrl: string, private algorithm: jwt.Algorithm,
8080
private issuer: string, private tokenInfo: FirebaseTokenInfo,
8181
private readonly app: FirebaseApp) {
82-
82+
8383
if (!validator.isURL(clientCertUrl)) {
8484
throw new FirebaseAuthError(
8585
AuthClientErrorCode.INVALID_ARGUMENT,
@@ -135,10 +135,11 @@ export class FirebaseTokenVerifier {
135135
* Verifies the format and signature of a Firebase Auth JWT token.
136136
*
137137
* @param {string} jwtToken The Firebase Auth JWT token to verify.
138+
* @param {boolean=} isEmulator Whether to accept Auth Emulator tokens.
138139
* @return {Promise<DecodedIdToken>} A promise fulfilled with the decoded claims of the Firebase Auth ID
139140
* token.
140141
*/
141-
public verifyJWT(jwtToken: string): Promise<DecodedIdToken> {
142+
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> {
142143
if (!validator.isString(jwtToken)) {
143144
throw new FirebaseAuthError(
144145
AuthClientErrorCode.INVALID_ARGUMENT,
@@ -148,19 +149,15 @@ export class FirebaseTokenVerifier {
148149

149150
return util.findProjectId(this.app)
150151
.then((projectId) => {
151-
return this.verifyJWTWithProjectId(jwtToken, projectId);
152+
return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator);
152153
});
153154
}
154155

155-
/**
156-
* Override the JWT signing algorithm.
157-
* @param algorithm the new signing algorithm.
158-
*/
159-
public setAlgorithm(algorithm: jwt.Algorithm): void {
160-
this.algorithm = algorithm;
161-
}
162-
163-
private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise<DecodedIdToken> {
156+
private verifyJWTWithProjectId(
157+
jwtToken: string,
158+
projectId: string | null,
159+
isEmulator: boolean
160+
): Promise<DecodedIdToken> {
164161
if (!validator.isNonEmptyString(projectId)) {
165162
throw new FirebaseAuthError(
166163
AuthClientErrorCode.INVALID_CREDENTIAL,
@@ -185,7 +182,7 @@ export class FirebaseTokenVerifier {
185182
if (!fullDecodedToken) {
186183
errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` +
187184
`which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage;
188-
} else if (typeof header.kid === 'undefined' && this.algorithm !== 'none') {
185+
} else if (!isEmulator && typeof header.kid === 'undefined') {
189186
const isCustomToken = (payload.aud === FIREBASE_AUDIENCE);
190187
const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d);
191188

@@ -200,7 +197,7 @@ export class FirebaseTokenVerifier {
200197
}
201198

202199
errorMessage += verifyJwtTokenDocsMessage;
203-
} else if (header.alg !== this.algorithm) {
200+
} else if (!isEmulator && header.alg !== this.algorithm) {
204201
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' +
205202
'"' + header.alg + '".' + verifyJwtTokenDocsMessage;
206203
} else if (payload.aud !== projectId) {
@@ -209,7 +206,7 @@ export class FirebaseTokenVerifier {
209206
verifyJwtTokenDocsMessage;
210207
} else if (payload.iss !== this.issuer + projectId) {
211208
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
212-
`"${this.issuer}"` + projectId + '" but got "' +
209+
`"${this.issuer}` + projectId + '" but got "' +
213210
payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage;
214211
} else if (typeof payload.sub !== 'string') {
215212
errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
@@ -223,9 +220,8 @@ export class FirebaseTokenVerifier {
223220
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
224221
}
225222

226-
// When the algorithm is set to 'none' there will be no signature and therefore we don't check
227-
// the public keys.
228-
if (this.algorithm === 'none') {
223+
if (isEmulator) {
224+
// Signature checks skipped for emulator; no need to fetch public keys.
229225
return this.verifyJwtSignatureWithKey(jwtToken, null);
230226
}
231227

@@ -257,26 +253,29 @@ export class FirebaseTokenVerifier {
257253
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
258254
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
259255
return new Promise((resolve, reject) => {
260-
jwt.verify(jwtToken, publicKey || '', {
261-
algorithms: [this.algorithm],
262-
}, (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => {
263-
if (error) {
264-
if (error.name === 'TokenExpiredError') {
265-
const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
266-
` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
267-
verifyJwtTokenDocsMessage;
268-
return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage));
269-
} else if (error.name === 'JsonWebTokenError') {
270-
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
271-
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
256+
const verifyOptions: jwt.VerifyOptions = {};
257+
if (publicKey !== null) {
258+
verifyOptions.algorithms = [this.algorithm];
259+
}
260+
jwt.verify(jwtToken, publicKey || '', verifyOptions,
261+
(error: jwt.VerifyErrors | null, decodedToken: object | undefined) => {
262+
if (error) {
263+
if (error.name === 'TokenExpiredError') {
264+
const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
265+
` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
266+
verifyJwtTokenDocsMessage;
267+
return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage));
268+
} else if (error.name === 'JsonWebTokenError') {
269+
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
270+
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
271+
}
272+
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
273+
} else {
274+
const decodedIdToken = (decodedToken as DecodedIdToken);
275+
decodedIdToken.uid = decodedIdToken.sub;
276+
resolve(decodedIdToken);
272277
}
273-
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
274-
} else {
275-
const decodedIdToken = (decodedToken as DecodedIdToken);
276-
decodedIdToken.uid = decodedIdToken.sub;
277-
resolve(decodedIdToken);
278-
}
279-
});
278+
});
280279
});
281280
}
282281

0 commit comments

Comments
 (0)