Skip to content

chore: Move token verification logic to util #1189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import * as validator from '../utils/validator';
import { auth } from './index';
import {
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier
} from './token-verifier';
} from '../utils/token-verifier';
import {
SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
} from './auth-config';
Expand Down
20 changes: 19 additions & 1 deletion src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,28 @@ export class AppErrorCodes {
public static UNABLE_TO_PARSE_RESPONSE = 'unable-to-parse-response';
}

/**
* Base class for client error codes and their default messages.
*/
export class BaseClientErrorCode {
public static INVALID_ARGUMENT = {
code: 'argument-error',
message: 'Invalid argument provided.',
};
public static INVALID_CREDENTIAL = {
code: 'invalid-credential',
message: 'Invalid credential object provided.',
};
public static INTERNAL_ERROR = {
code: 'internal-error',
message: 'An internal error has occurred.',
};
}

/**
* Auth client error codes and their default messages.
*/
export class AuthClientErrorCode {
export class AuthClientErrorCode extends BaseClientErrorCode {
public static BILLING_NOT_ENABLED = {
code: 'billing-not-enabled',
message: 'Feature requires billing to be enabled.',
Expand Down
77 changes: 43 additions & 34 deletions src/auth/token-verifier.ts → src/utils/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
* limitations under the License.
*/

import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error';
import * as util from '../utils/index';
import * as validator from '../utils/validator';
import * as util from './index';
import * as validator from './validator';
import * as jwt from 'jsonwebtoken';
import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request';
import { HttpClient, HttpRequestConfig, HttpError } from './api-request';
import { FirebaseApp } from '../firebase-app';
import { auth } from './index';
import { ErrorInfo, PrefixedFirebaseError,
BaseClientErrorCode, AuthClientErrorCode, FirebaseAuthError } from './error';
import { auth } from '../auth/index';

import DecodedIdToken = auth.DecodedIdToken;

Expand All @@ -43,6 +44,8 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = {
jwtName: 'Firebase ID token',
shortName: 'ID token',
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
};

/** User facing token information related to the Firebase session cookie. */
Expand All @@ -52,6 +55,8 @@ export const SESSION_COOKIE_INFO: FirebaseTokenInfo = {
jwtName: 'Firebase session cookie',
shortName: 'session cookie',
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
};

/** Interface that defines token related user facing information. */
Expand All @@ -66,6 +71,10 @@ export interface FirebaseTokenInfo {
shortName: string;
/** JWT Expiration error code. */
expiredErrorCode: ErrorInfo;
/** Generic error code type. */
errorCodeType: typeof BaseClientErrorCode;
/** Error type. */
errorType: new (info: ErrorInfo, message?: string) => PrefixedFirebaseError;
}

/**
Expand All @@ -81,48 +90,48 @@ export class FirebaseTokenVerifier {
private readonly app: FirebaseApp) {

if (!validator.isURL(clientCertUrl)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The provided public client certificate URL is an invalid URL.',
);

} else if (!validator.isNonEmptyString(algorithm)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The provided JWT algorithm is an empty string.',
);
} else if (!validator.isURL(issuer)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The provided JWT issuer is an invalid URL.',
);
} else if (!validator.isNonNullObject(tokenInfo)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The provided JWT information is not an object or null.',
);
} else if (!validator.isURL(tokenInfo.url)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The provided JWT verification documentation URL is invalid.',
);
} else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The JWT verify API name must be a non-empty string.',
);
} else if (!validator.isNonEmptyString(tokenInfo.jwtName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The JWT public full name must be a non-empty string.',
);
} else if (!validator.isNonEmptyString(tokenInfo.shortName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The JWT public short name must be a non-empty string.',
);
} else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
'The JWT expiration error code must be a non-null ErrorInfo object.',
);
}
Expand All @@ -141,8 +150,8 @@ export class FirebaseTokenVerifier {
*/
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> {
if (!validator.isString(jwtToken)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
);
}
Expand All @@ -159,8 +168,8 @@ export class FirebaseTokenVerifier {
isEmulator: boolean
): Promise<DecodedIdToken> {
if (!validator.isNonEmptyString(projectId)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CREDENTIAL,
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_CREDENTIAL,
'Must initialize app with a cert credential or set your Firebase project ID as the ' +
`GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`,
);
Expand Down Expand Up @@ -217,7 +226,7 @@ export class FirebaseTokenVerifier {
verifyJwtTokenDocsMessage;
}
if (errorMessage) {
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage));
}

if (isEmulator) {
Expand All @@ -228,8 +237,8 @@ export class FirebaseTokenVerifier {
return this.fetchPublicKeys().then((publicKeys) => {
if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) {
return Promise.reject(
new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
`${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` +
`Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` +
'client app and try again.',
Expand Down Expand Up @@ -264,12 +273,12 @@ export class FirebaseTokenVerifier {
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));
return reject(new this.tokenInfo.errorType(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 this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage));
}
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, error.message));
} else {
const decodedIdToken = (decodedToken as DecodedIdToken);
decodedIdToken.uid = decodedIdToken.sub;
Expand Down Expand Up @@ -329,7 +338,7 @@ export class FirebaseTokenVerifier {
} else {
errorMessage += `${resp.text}`;
}
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage);
throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INTERNAL_ERROR, errorMessage);
}
throw err;
});
Expand Down
2 changes: 1 addition & 1 deletion test/unit/auth/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error';

import * as validator from '../../../src/utils/validator';
import { FirebaseTokenVerifier } from '../../../src/auth/token-verifier';
import { FirebaseTokenVerifier } from '../../../src/utils/token-verifier';
import {
OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
} from '../../../src/auth/auth-config';
Expand Down
40 changes: 38 additions & 2 deletions test/unit/auth/token-verifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import LegacyFirebaseTokenGenerator = require('firebase-token-generator');

import * as mocks from '../../resources/mocks';
import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator';
import * as verifier from '../../../src/auth/token-verifier';
import * as verifier from '../../../src/utils/token-verifier';

import { ServiceAccountCredential } from '../../../src/credential/credential-internal';
import { AuthClientErrorCode } from '../../../src/utils/error';
import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error';
import { FirebaseApp } from '../../../src/firebase-app';
import { Algorithm } from 'jsonwebtoken';

Expand Down Expand Up @@ -160,6 +160,8 @@ describe('FirebaseTokenVerifier', () => {
jwtName: 'Important Token',
shortName: 'token',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
},
app,
);
Expand Down Expand Up @@ -224,6 +226,8 @@ describe('FirebaseTokenVerifier', () => {
jwtName: 'Important Token',
shortName: 'token',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
},
app,
);
Expand All @@ -245,6 +249,8 @@ describe('FirebaseTokenVerifier', () => {
jwtName: invalidJwtName as any,
shortName: 'token',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
},
app,
);
Expand All @@ -266,6 +272,8 @@ describe('FirebaseTokenVerifier', () => {
jwtName: 'Important Token',
shortName: invalidShortName as any,
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
},
app,
);
Expand All @@ -287,6 +295,8 @@ describe('FirebaseTokenVerifier', () => {
jwtName: 'Important Token',
shortName: 'token',
expiredErrorCode: invalidExpiredErrorCode as any,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
},
app,
);
Expand All @@ -295,6 +305,32 @@ describe('FirebaseTokenVerifier', () => {
});
});

const errorTypes = [
{ type: FirebaseAuthError, code: AuthClientErrorCode },
];
errorTypes.forEach((errorType) => {
it('should throw with the correct error type set in token info', () => {
expect(() => {
new verifier.FirebaseTokenVerifier(
'https://www.example.com/publicKeys',
'RS256',
'https://www.example.com/issuer/',
{
url: 'https://docs.example.com/verify-tokens',
verifyApiName: 'verifyToken()',
jwtName: 'Important Token',
shortName: '',
expiredErrorCode: errorType.code.INVALID_ARGUMENT,
errorCodeType: errorType.code,
errorType: errorType.type,
},
app,
);
}).to.throw(errorType.type)
.with.property('code').and.match(/(auth|messaging)\/argument-error/);
});
});

describe('verifyJWT()', () => {
let mockedRequests: nock.Scope[] = [];

Expand Down