Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
},
"devDependencies": {
"@firebase/app": "^0.6.13",
"@firebase/auth": "^0.15.2",
"@firebase/auth": "^0.16.2",
"@firebase/auth-types": "^0.10.1",
"@microsoft/api-extractor": "^7.11.2",
"@types/bcrypt": "^2.0.0",
Expand Down
4 changes: 0 additions & 4 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2117,10 +2117,6 @@ function emulatorHost(): string | undefined {
/**
* When true the SDK should communicate with the Auth Emulator for all API
* calls and also produce unsigned tokens.
*
* This alone does <b>NOT<b> short-circuit ID Token verification.
* For security reasons that must be explicitly disabled through
* setJwtVerificationEnabled(false);
*/
export function useEmulator(): boolean {
return !!emulatorHost();
Expand Down
44 changes: 11 additions & 33 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import * as utils from '../utils/index';
import * as validator from '../utils/validator';
import { auth } from './index';
import {
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier
} from './token-verifier';
import {
SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
Expand Down Expand Up @@ -118,12 +118,12 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
return this.idTokenVerifier.verifyJWT(idToken)
.then((decodedIdToken: DecodedIdToken) => {
// Whether to check if the token was revoked.
if (!checkRevoked) {
return decodedIdToken;
if (checkRevoked || useEmulator()) {
return this.verifyDecodedJWTNotRevoked(
decodedIdToken,
AuthClientErrorCode.ID_TOKEN_REVOKED);
}
return this.verifyDecodedJWTNotRevoked(
decodedIdToken,
AuthClientErrorCode.ID_TOKEN_REVOKED);
return decodedIdToken;
});
}

Expand Down Expand Up @@ -446,12 +446,12 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
return this.sessionCookieVerifier.verifyJWT(sessionCookie)
.then((decodedIdToken: DecodedIdToken) => {
// Whether to check if the token was revoked.
if (!checkRevoked) {
return decodedIdToken;
if (checkRevoked || useEmulator()) {
return this.verifyDecodedJWTNotRevoked(
decodedIdToken,
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
}
return this.verifyDecodedJWTNotRevoked(
decodedIdToken,
AuthClientErrorCode.SESSION_COOKIE_REVOKED);
return decodedIdToken;
});
}

Expand Down Expand Up @@ -675,28 +675,6 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> implements BaseAuthI
return decodedIdToken;
});
}

/**
* Enable or disable ID token verification. This is used to safely short-circuit token verification with the
* Auth emulator. When disabled ONLY unsigned tokens will pass verification, production tokens will not pass.
*
* WARNING: This is a dangerous method that will compromise your app's security and break your app in
* production. Developers should never call this method, it is for internal testing use only.
*
* @internal
*/
// @ts-expect-error: this method appears unused but is used privately.
private setJwtVerificationEnabled(enabled: boolean): void {
if (!enabled && !useEmulator()) {
// We only allow verification to be disabled in conjunction with
// the emulator environment variable.
throw new Error('This method is only available when connected to the Authentication emulator.');
}

const algorithm = enabled ? ALGORITHM_RS256 : 'none';
this.idTokenVerifier.setAlgorithm(algorithm);
this.sessionCookieVerifier.setAlgorithm(algorithm);
}
}


Expand Down
63 changes: 29 additions & 34 deletions src/auth/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { FirebaseApp } from '../firebase-app';
import { auth } from './index';

import DecodedIdToken = auth.DecodedIdToken;
import { useEmulator } from './auth-api-request';

// Audience to use for Firebase Auth Custom tokens
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
Expand Down Expand Up @@ -79,7 +80,7 @@ export class FirebaseTokenVerifier {
constructor(private clientCertUrl: string, private algorithm: jwt.Algorithm,
private issuer: string, private tokenInfo: FirebaseTokenInfo,
private readonly app: FirebaseApp) {

if (!validator.isURL(clientCertUrl)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
Expand Down Expand Up @@ -152,14 +153,6 @@ export class FirebaseTokenVerifier {
});
}

/**
* Override the JWT signing algorithm.
* @param algorithm the new signing algorithm.
*/
public setAlgorithm(algorithm: jwt.Algorithm): void {
this.algorithm = algorithm;
}

private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise<DecodedIdToken> {
if (!validator.isNonEmptyString(projectId)) {
throw new FirebaseAuthError(
Expand All @@ -185,7 +178,7 @@ export class FirebaseTokenVerifier {
if (!fullDecodedToken) {
errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` +
`which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage;
} else if (typeof header.kid === 'undefined' && this.algorithm !== 'none') {
} else if (!useEmulator() && typeof header.kid === 'undefined') {
const isCustomToken = (payload.aud === FIREBASE_AUDIENCE);
const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d);

Expand All @@ -200,7 +193,7 @@ export class FirebaseTokenVerifier {
}

errorMessage += verifyJwtTokenDocsMessage;
} else if (header.alg !== this.algorithm) {
} else if (!useEmulator() && header.alg !== this.algorithm) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' +
'"' + header.alg + '".' + verifyJwtTokenDocsMessage;
} else if (payload.aud !== projectId) {
Expand All @@ -209,7 +202,7 @@ export class FirebaseTokenVerifier {
verifyJwtTokenDocsMessage;
} else if (payload.iss !== this.issuer + projectId) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
`"${this.issuer}"` + projectId + '" but got "' +
`"${this.issuer}` + projectId + '" but got "' +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: took me a while staring at this diff because of the mixed use of string templates and string concatenation via +. No need to do anything though, probably not in scope.

payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage;
} else if (typeof payload.sub !== 'string') {
errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
Expand All @@ -223,9 +216,8 @@ export class FirebaseTokenVerifier {
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
}

// When the algorithm is set to 'none' there will be no signature and therefore we don't check
// the public keys.
if (this.algorithm === 'none') {
if (useEmulator()) {
// Signature checks skipped for emulator; no need to fetch public keys.
return this.verifyJwtSignatureWithKey(jwtToken, null);
}

Expand Down Expand Up @@ -257,26 +249,29 @@ export class FirebaseTokenVerifier {
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
return new Promise((resolve, reject) => {
jwt.verify(jwtToken, publicKey || '', {
algorithms: [this.algorithm],
}, (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => {
if (error) {
if (error.name === 'TokenExpiredError') {
const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
verifyJwtTokenDocsMessage;
return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage));
} else if (error.name === 'JsonWebTokenError') {
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
const verifyOptions: jwt.VerifyOptions = {};
if (!useEmulator()) {
verifyOptions.algorithms = [this.algorithm];
}
jwt.verify(jwtToken, publicKey || '', verifyOptions,
(error: jwt.VerifyErrors | null, decodedToken: object | undefined) => {
if (error) {
if (error.name === 'TokenExpiredError') {
const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
verifyJwtTokenDocsMessage;
return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage));
} else if (error.name === 'JsonWebTokenError') {
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
}
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
} else {
const decodedIdToken = (decodedToken as DecodedIdToken);
decodedIdToken.uid = decodedIdToken.sub;
resolve(decodedIdToken);
}
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
} else {
const decodedIdToken = (decodedToken as DecodedIdToken);
decodedIdToken.uid = decodedIdToken.sub;
resolve(decodedIdToken);
}
});
});
});
}

Expand Down
Loading