Skip to content

feat(auth): Add support for Auth Emulator #1044

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

Merged
merged 15 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 13 additions & 13 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 @@ -73,7 +73,7 @@
"@types/chai": "^4.0.0",
"@types/chai-as-promised": "^7.1.0",
"@types/firebase-token-generator": "^2.0.28",
"@types/jsonwebtoken": "^7.2.8",
"@types/jsonwebtoken": "^8.5.0",
"@types/lodash": "^4.14.104",
"@types/minimist": "^1.2.0",
"@types/mocha": "^2.2.48",
Expand Down
27 changes: 25 additions & 2 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,19 @@ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100;
const FIREBASE_AUTH_BASE_URL_FORMAT =
'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';

/** Firebase Auth base URlLformat when using the auth emultor. */
const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT =
'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';

/** The Firebase Auth backend multi-tenancy base URL format. */
const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');

/** Firebase Auth base URL format when using the auth emultor with multi-tenancy. */
const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace(
'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');


/** Maximum allowed number of tenants to download at one time. */
const MAX_LIST_TENANT_PAGE_SIZE = 1000;

Expand Down Expand Up @@ -121,7 +130,14 @@ class AuthResourceUrlBuilder {
* @constructor
*/
constructor(protected app: FirebaseApp, protected version: string = 'v1') {
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST;
if (emulatorHost) {
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, {
host: emulatorHost
});
} else {
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
}
}

/**
Expand Down Expand Up @@ -181,7 +197,14 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
*/
constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) {
super(app, version);
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST
if (emulatorHost) {
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, {
host: emulatorHost
});
} else {
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
}
}

/**
Expand Down
44 changes: 40 additions & 4 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier,
} from './identifier';
import { FirebaseApp } from '../firebase-app';
import { FirebaseTokenGenerator, cryptoSignerFromApp } from './token-generator';
import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator';
import {
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler,
} from './auth-api-request';
Expand All @@ -31,7 +31,9 @@ import {

import * as utils from '../utils/index';
import * as validator from '../utils/validator';
import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier';
import {
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256
} from './token-verifier';
import { ActionCodeSettings } from './action-code-settings-builder';
import {
AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest,
Expand Down Expand Up @@ -141,7 +143,7 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
if (tokenGenerator) {
this.tokenGenerator = tokenGenerator;
} else {
const cryptoSigner = cryptoSignerFromApp(app);
const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
}

Expand Down Expand Up @@ -735,6 +737,28 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
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 All @@ -752,7 +776,7 @@ export class TenantAwareAuth extends BaseAuth<TenantAwareAuthRequestHandler> {
* @constructor
*/
constructor(app: FirebaseApp, tenantId: string) {
const cryptoSigner = cryptoSignerFromApp(app);
const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId);
super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator);
utils.addReadonlyGetter(this, 'tenantId', tenantId);
Expand Down Expand Up @@ -868,3 +892,15 @@ export class Auth extends BaseAuth<AuthRequestHandler> implements FirebaseServic
return this.tenantManager_;
}
}

/**
* 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);
*/
function useEmulator(): boolean {
return !!process.env.FIREBASE_AUTH_EMULATOR_HOST;
}
43 changes: 40 additions & 3 deletions src/auth/token-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '

import * as validator from '../utils/validator';
import { toWebSafeBase64 } from '../utils';
import { Algorithm } from 'jsonwebtoken';


const ALGORITHM_RS256 = 'RS256';
const ALGORITHM_RS256: Algorithm = 'RS256' as const;
const ALGORITHM_NONE: Algorithm = 'none' as const;

const ONE_HOUR_IN_SECONDS = 60 * 60;

// List of blacklisted claims which cannot be provided when creating a custom token
Expand All @@ -39,6 +42,12 @@ const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identit
* CryptoSigner interface represents an object that can be used to sign JWTs.
*/
export interface CryptoSigner {

/**
* The name of the signing algorithm.
*/
readonly algorithm: Algorithm;

/**
* Cryptographically signs a buffer of data.
*
Expand Down Expand Up @@ -82,6 +91,8 @@ interface JWTBody {
* sign data. Performs all operations locally, and does not make any RPC calls.
*/
export class ServiceAccountSigner implements CryptoSigner {

algorithm = ALGORITHM_RS256;

/**
* Creates a new CryptoSigner instance from the given service account credential.
Expand Down Expand Up @@ -124,6 +135,8 @@ export class ServiceAccountSigner implements CryptoSigner {
* @see https://cloud.google.com/compute/docs/storing-retrieving-metadata
*/
export class IAMSigner implements CryptoSigner {
algorithm = ALGORITHM_RS256;

private readonly httpClient: AuthorizedHttpClient;
private serviceAccountId?: string;

Expand Down Expand Up @@ -215,6 +228,29 @@ export class IAMSigner implements CryptoSigner {
}
}

/**
* A CryptoSigner implementation that is used when communicating with the Auth emulator.
* It produces unsigned tokens.
*/
export class EmulatedSigner implements CryptoSigner {

algorithm = ALGORITHM_NONE;

/**
* @inheritDoc
*/
public sign(_: Buffer): Promise<Buffer> {
return Promise.resolve(Buffer.from(''));
}

/**
* @inheritDoc
*/
public getAccountId(): Promise<string> {
return Promise.resolve('[email protected]');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not so sure about this ... could we do something better here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

This is probably the best thing we can do

}
}

/**
* Create a new CryptoSigner instance for the given app. If the app has been initialized with a service
* account credential, creates a ServiceAccountSigner. Otherwise creates an IAMSigner.
Expand Down Expand Up @@ -250,7 +286,7 @@ export class FirebaseTokenGenerator {
'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.',
);
}
if (typeof tenantId !== 'undefined' && !validator.isNonEmptyString(tenantId)) {
if (typeof this.tenantId !== 'undefined' && !validator.isNonEmptyString(this.tenantId)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'`tenantId` argument must be a non-empty string.');
Expand Down Expand Up @@ -298,7 +334,7 @@ export class FirebaseTokenGenerator {
}
return this.signer.getAccountId().then((account) => {
const header: JWTHeader = {
alg: ALGORITHM_RS256,
alg: this.signer.algorithm,
typ: 'JWT',
};
const iat = Math.floor(Date.now() / 1000);
Expand All @@ -319,6 +355,7 @@ export class FirebaseTokenGenerator {
}
const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`;
const signPromise = this.signer.sign(Buffer.from(token));

return Promise.all([token, signPromise]);
}).then(([token, signature]) => {
return `${token}.${this.encodeSegment(signature)}`;
Expand Down
Loading