Skip to content
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