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 4 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
5 changes: 2 additions & 3 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/erro
import * as utils from '../utils/index';
import * as validator from '../utils/validator';
import { auth } from './index';
import {
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier
} from './token-verifier';
import { FirebaseTokenVerifier } from '../utils/token-verifier';
import { createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier-util';
import {
SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse,
} from './auth-config';
Expand Down
89 changes: 89 additions & 0 deletions src/auth/token-verifier-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*!
* Copyright 2021 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { FirebaseApp } from '../firebase-app';
import { AuthClientErrorCode, ErrorCodeConfig, FirebaseAuthError } from '../utils/error';
import { FirebaseTokenInfo, FirebaseTokenVerifier } from '../utils/token-verifier';

const ALGORITHM_RS256 = 'RS256';

// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
// Auth ID tokens)
const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]';

// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon.
const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys';

/** Error codes that matches the FirebaseAuthError type */
const AUTH_ERROR_CODE_CONFIG: ErrorCodeConfig = {
invalidArg: AuthClientErrorCode.INVALID_ARGUMENT,
invalidCredential: AuthClientErrorCode.INVALID_CREDENTIAL,
internalError: AuthClientErrorCode.INTERNAL_ERROR,
}

/** User facing token information related to the Firebase ID token. */
export const ID_TOKEN_INFO: FirebaseTokenInfo = {
url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens',
verifyApiName: 'verifyIdToken()',
jwtName: 'Firebase ID token',
shortName: 'ID token',
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED,
errorCodeConfig: AUTH_ERROR_CODE_CONFIG,
errorType: FirebaseAuthError,
};

/** User facing token information related to the Firebase session cookie. */
export const SESSION_COOKIE_INFO: FirebaseTokenInfo = {
url: 'https://firebase.google.com/docs/auth/admin/manage-cookies',
verifyApiName: 'verifySessionCookie()',
jwtName: 'Firebase session cookie',
shortName: 'session cookie',
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
errorCodeConfig: AUTH_ERROR_CODE_CONFIG,
errorType: FirebaseAuthError,
};

/**
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
*
* @param {FirebaseApp} app Firebase app instance.
* @return {FirebaseTokenVerifier}
*/
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
CLIENT_CERT_URL,
ALGORITHM_RS256,
'https://securetoken.google.com/',
ID_TOKEN_INFO,
app
);
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
*
* @param {FirebaseApp} app Firebase app instance.
* @return {FirebaseTokenVerifier}
*/
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
SESSION_COOKIE_CERT_URL,
ALGORITHM_RS256,
'https://session.firebase.google.com/',
SESSION_COOKIE_INFO,
app
);
}
9 changes: 9 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ interface ServerToClientCode {
[code: string]: string;
}

/**
* Defines a type that stores commonly used error codes.
*/
export interface ErrorCodeConfig {
invalidArg: ErrorInfo;
invalidCredential: ErrorInfo;
internalError: ErrorInfo;
}

/**
* Firebase error code structure. This extends Error.
*
Expand Down
143 changes: 31 additions & 112 deletions src/auth/token-verifier.ts → src/utils/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,19 @@
* 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 { ErrorCodeConfig, ErrorInfo, PrefixedFirebaseError } from './error';
import { auth } from '../auth/index';

import DecodedIdToken = auth.DecodedIdToken;

// Audience to use for Firebase Auth Custom tokens
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';

export const ALGORITHM_RS256 = 'RS256';

// URL containing the public keys for the Google certs (whose private keys are used to sign Firebase
// Auth ID tokens)
const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]';

// URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon.
const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys';

/** User facing token information related to the Firebase ID token. */
export const ID_TOKEN_INFO: FirebaseTokenInfo = {
url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens',
verifyApiName: 'verifyIdToken()',
jwtName: 'Firebase ID token',
shortName: 'ID token',
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED,
};

/** User facing token information related to the Firebase session cookie. */
export const SESSION_COOKIE_INFO: FirebaseTokenInfo = {
url: 'https://firebase.google.com/docs/auth/admin/manage-cookies',
verifyApiName: 'verifySessionCookie()',
jwtName: 'Firebase session cookie',
shortName: 'session cookie',
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
};

/** Interface that defines token related user facing information. */
export interface FirebaseTokenInfo {
/** Documentation URL. */
Expand All @@ -66,6 +39,10 @@ export interface FirebaseTokenInfo {
shortName: string;
/** JWT Expiration error code. */
expiredErrorCode: ErrorInfo;
/** Error code config of the public error type. */
errorCodeConfig: ErrorCodeConfig;
/** Public error type. */
errorType: new (info: ErrorInfo, message?: string) => PrefixedFirebaseError;
}

/**
Expand All @@ -81,50 +58,24 @@ export class FirebaseTokenVerifier {
private readonly app: FirebaseApp) {

if (!validator.isURL(clientCertUrl)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The provided public client certificate URL is an invalid URL.',
);
throw new Error('The provided public client certificate URL is an invalid URL.');
} else if (!validator.isNonEmptyString(algorithm)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The provided JWT algorithm is an empty string.',
);
throw new Error('The provided JWT algorithm is an empty string.');
} else if (!validator.isURL(issuer)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The provided JWT issuer is an invalid URL.',
);
throw new Error('The provided JWT issuer is an invalid URL.');
} else if (!validator.isNonNullObject(tokenInfo)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The provided JWT information is not an object or null.',
);
throw new Error('The provided JWT information is not an object or null.');
} else if (!validator.isURL(tokenInfo.url)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The provided JWT verification documentation URL is invalid.',
);
throw new Error('The provided JWT verification documentation URL is invalid.');
} else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The JWT verify API name must be a non-empty string.',
);
throw new Error('The JWT verify API name must be a non-empty string.');
} else if (!validator.isNonEmptyString(tokenInfo.jwtName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'The JWT public full name must be a non-empty string.',
);
throw new Error('The JWT public full name must be a non-empty string.');
} else if (!validator.isNonEmptyString(tokenInfo.shortName)) {
throw new FirebaseAuthError(
AuthClientErrorCode.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,
'The JWT expiration error code must be a non-null ErrorInfo object.',
);
throw new Error('The JWT public short name must be a non-empty string.');
} else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) ||
!('code' in tokenInfo.expiredErrorCode)) {
throw new Error('The JWT expiration error code must be a non-null ErrorInfo object.');
}
this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a';

Expand All @@ -141,8 +92,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.errorCodeConfig.invalidArg,
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
);
}
Expand All @@ -159,8 +110,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.errorCodeConfig.invalidCredential,
'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 +168,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.errorCodeConfig.invalidArg, errorMessage));
}

if (isEmulator) {
Expand All @@ -228,8 +179,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.errorCodeConfig.invalidArg,
`${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 +215,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.errorCodeConfig.invalidArg, errorMessage));
}
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, error.message));
} else {
const decodedIdToken = (decodedToken as DecodedIdToken);
decodedIdToken.uid = decodedIdToken.sub;
Expand Down Expand Up @@ -329,41 +280,9 @@ export class FirebaseTokenVerifier {
} else {
errorMessage += `${resp.text}`;
}
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage);
throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.internalError, errorMessage);
}
throw err;
});
}
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
*
* @param {FirebaseApp} app Firebase app instance.
* @return {FirebaseTokenVerifier}
*/
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
CLIENT_CERT_URL,
ALGORITHM_RS256,
'https://securetoken.google.com/',
ID_TOKEN_INFO,
app
);
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
*
* @param {FirebaseApp} app Firebase app instance.
* @return {FirebaseTokenVerifier}
*/
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
SESSION_COOKIE_CERT_URL,
ALGORITHM_RS256,
'https://session.firebase.google.com/',
SESSION_COOKIE_INFO,
app
);
}
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
Loading