Skip to content
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
Prev Previous commit
Next Next commit
Moved Auth related items to token-verifier-util
  • Loading branch information
lahirumaramba committed Mar 9, 2021
commit 6719cfae202076bfe33062887c77bb6a5fb29009
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 '../utils/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
);
}
29 changes: 10 additions & 19 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 Expand Up @@ -338,28 +347,10 @@ 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 extends BaseClientErrorCode {
export class AuthClientErrorCode {
public static BILLING_NOT_ENABLED = {
code: 'billing-not-enabled',
message: 'Feature requires billing to be enabled.',
Expand Down
105 changes: 21 additions & 84 deletions src/utils/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,14 @@ import * as validator from './validator';
import * as jwt from 'jsonwebtoken';
import { HttpClient, HttpRequestConfig, HttpError } from './api-request';
import { FirebaseApp } from '../firebase-app';
import { ErrorInfo, PrefixedFirebaseError,
BaseClientErrorCode, AuthClientErrorCode, FirebaseAuthError } from './error';
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,
errorCodeType: AuthClientErrorCode,
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,
errorCodeType: AuthClientErrorCode,
errorType: FirebaseAuthError,
};

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

Expand All @@ -90,48 +58,49 @@ export class FirebaseTokenVerifier {
private readonly app: FirebaseApp) {

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

} else if (!validator.isNonEmptyString(algorithm)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The provided JWT algorithm is an empty string.',
);
} else if (!validator.isURL(issuer)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The provided JWT issuer is an invalid URL.',
);
} else if (!validator.isNonNullObject(tokenInfo)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The provided JWT information is not an object or null.',
);
} else if (!validator.isURL(tokenInfo.url)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The provided JWT verification documentation URL is invalid.',
);
} else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The JWT verify API name must be a non-empty string.',
);
} else if (!validator.isNonEmptyString(tokenInfo.jwtName)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The JWT public full name must be a non-empty string.',
);
} else if (!validator.isNonEmptyString(tokenInfo.shortName)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The JWT public short name must be a non-empty string.',
);
} else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
'The JWT expiration error code must be a non-null ErrorInfo object.',
);
}
Expand All @@ -151,7 +120,7 @@ export class FirebaseTokenVerifier {
public verifyJWT(jwtToken: string, isEmulator = false): Promise<DecodedIdToken> {
if (!validator.isString(jwtToken)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
this.tokenInfo.errorCodeConfig.invalidArg,
`First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`,
);
}
Expand All @@ -169,7 +138,7 @@ export class FirebaseTokenVerifier {
): Promise<DecodedIdToken> {
if (!validator.isNonEmptyString(projectId)) {
throw new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_CREDENTIAL,
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 @@ -226,7 +195,7 @@ export class FirebaseTokenVerifier {
verifyJwtTokenDocsMessage;
}
if (errorMessage) {
return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage));
return Promise.reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage));
}

if (isEmulator) {
Expand All @@ -238,7 +207,7 @@ export class FirebaseTokenVerifier {
if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) {
return Promise.reject(
new this.tokenInfo.errorType(
this.tokenInfo.errorCodeType.INVALID_ARGUMENT,
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 @@ -276,9 +245,9 @@ export class FirebaseTokenVerifier {
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 this.tokenInfo.errorType(this.tokenInfo.errorCodeType.INVALID_ARGUMENT, errorMessage));
return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeConfig.invalidArg, errorMessage));
}
return reject(new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.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 @@ -338,41 +307,9 @@ export class FirebaseTokenVerifier {
} else {
errorMessage += `${resp.text}`;
}
throw new this.tokenInfo.errorType(this.tokenInfo.errorCodeType.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
);
}
Loading