Skip to content

Commit dbb0c78

Browse files
authored
Add support for Auth Emulator (#1044)
* Basic URL replacement and custom tokens * Fix test * Tests actually pass * Mock verifyIdToken * Add another test * Small style change * Small simplification * Significant cleanup of all the useEmulator stuff * Make sure we don't use env var to short-circuit ID Token verification * Make lint pass * Add unit tests that go through the Auth interface * Hiranya nits * Make private method even more private and scary, require env * Make the private method throw * Fix test
1 parent 738250d commit dbb0c78

File tree

10 files changed

+303
-54
lines changed

10 files changed

+303
-54
lines changed

package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"@types/chai": "^4.0.0",
7474
"@types/chai-as-promised": "^7.1.0",
7575
"@types/firebase-token-generator": "^2.0.28",
76-
"@types/jsonwebtoken": "^7.2.8",
76+
"@types/jsonwebtoken": "^8.5.0",
7777
"@types/lodash": "^4.14.104",
7878
"@types/minimist": "^1.2.0",
7979
"@types/mocha": "^2.2.48",

src/auth/auth-api-request.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,19 @@ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100;
8989
const FIREBASE_AUTH_BASE_URL_FORMAT =
9090
'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';
9191

92+
/** Firebase Auth base URlLformat when using the auth emultor. */
93+
const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT =
94+
'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';
95+
9296
/** The Firebase Auth backend multi-tenancy base URL format. */
9397
const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
9498
'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');
9599

100+
/** Firebase Auth base URL format when using the auth emultor with multi-tenancy. */
101+
const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace(
102+
'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');
103+
104+
96105
/** Maximum allowed number of tenants to download at one time. */
97106
const MAX_LIST_TENANT_PAGE_SIZE = 1000;
98107

@@ -121,7 +130,14 @@ class AuthResourceUrlBuilder {
121130
* @constructor
122131
*/
123132
constructor(protected app: FirebaseApp, protected version: string = 'v1') {
124-
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
133+
const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST;
134+
if (emulatorHost) {
135+
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, {
136+
host: emulatorHost
137+
});
138+
} else {
139+
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
140+
}
125141
}
126142

127143
/**
@@ -181,7 +197,14 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
181197
*/
182198
constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) {
183199
super(app, version);
184-
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
200+
const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST
201+
if (emulatorHost) {
202+
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, {
203+
host: emulatorHost
204+
});
205+
} else {
206+
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
207+
}
185208
}
186209

187210
/**

src/auth/auth.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier,
2020
} from './identifier';
2121
import { FirebaseApp } from '../firebase-app';
22-
import { FirebaseTokenGenerator, cryptoSignerFromApp } from './token-generator';
22+
import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator';
2323
import {
2424
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler,
2525
} from './auth-api-request';
@@ -31,7 +31,9 @@ import {
3131

3232
import * as utils from '../utils/index';
3333
import * as validator from '../utils/validator';
34-
import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier';
34+
import {
35+
FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256
36+
} from './token-verifier';
3537
import { ActionCodeSettings } from './action-code-settings-builder';
3638
import {
3739
AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest,
@@ -141,7 +143,7 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
141143
if (tokenGenerator) {
142144
this.tokenGenerator = tokenGenerator;
143145
} else {
144-
const cryptoSigner = cryptoSignerFromApp(app);
146+
const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
145147
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
146148
}
147149

@@ -735,6 +737,28 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
735737
return decodedIdToken;
736738
});
737739
}
740+
741+
/**
742+
* Enable or disable ID token verification. This is used to safely short-circuit token verification with the
743+
* Auth emulator. When disabled ONLY unsigned tokens will pass verification, production tokens will not pass.
744+
*
745+
* WARNING: This is a dangerous method that will compromise your app's security and break your app in
746+
* production. Developers should never call this method, it is for internal testing use only.
747+
*
748+
* @internal
749+
*/
750+
// @ts-expect-error: this method appears unused but is used privately.
751+
private setJwtVerificationEnabled(enabled: boolean): void {
752+
if (!enabled && !useEmulator()) {
753+
// We only allow verification to be disabled in conjunction with
754+
// the emulator environment variable.
755+
throw new Error('This method is only available when connected to the Authentication emulator.');
756+
}
757+
758+
const algorithm = enabled ? ALGORITHM_RS256 : 'none';
759+
this.idTokenVerifier.setAlgorithm(algorithm);
760+
this.sessionCookieVerifier.setAlgorithm(algorithm);
761+
}
738762
}
739763

740764

@@ -752,7 +776,7 @@ export class TenantAwareAuth extends BaseAuth<TenantAwareAuthRequestHandler> {
752776
* @constructor
753777
*/
754778
constructor(app: FirebaseApp, tenantId: string) {
755-
const cryptoSigner = cryptoSignerFromApp(app);
779+
const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
756780
const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId);
757781
super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator);
758782
utils.addReadonlyGetter(this, 'tenantId', tenantId);
@@ -868,3 +892,15 @@ export class Auth extends BaseAuth<AuthRequestHandler> implements FirebaseServic
868892
return this.tenantManager_;
869893
}
870894
}
895+
896+
/**
897+
* When true the SDK should communicate with the Auth Emulator for all API
898+
* calls and also produce unsigned tokens.
899+
*
900+
* This alone does <b>NOT<b> short-circuit ID Token verification.
901+
* For security reasons that must be explicitly disabled through
902+
* setJwtVerificationEnabled(false);
903+
*/
904+
function useEmulator(): boolean {
905+
return !!process.env.FIREBASE_AUTH_EMULATOR_HOST;
906+
}

src/auth/token-generator.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '
2121

2222
import * as validator from '../utils/validator';
2323
import { toWebSafeBase64 } from '../utils';
24+
import { Algorithm } from 'jsonwebtoken';
2425

2526

26-
const ALGORITHM_RS256 = 'RS256';
27+
const ALGORITHM_RS256: Algorithm = 'RS256' as const;
28+
const ALGORITHM_NONE: Algorithm = 'none' as const;
29+
2730
const ONE_HOUR_IN_SECONDS = 60 * 60;
2831

2932
// List of blacklisted claims which cannot be provided when creating a custom token
@@ -39,6 +42,12 @@ const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identit
3942
* CryptoSigner interface represents an object that can be used to sign JWTs.
4043
*/
4144
export interface CryptoSigner {
45+
46+
/**
47+
* The name of the signing algorithm.
48+
*/
49+
readonly algorithm: Algorithm;
50+
4251
/**
4352
* Cryptographically signs a buffer of data.
4453
*
@@ -82,6 +91,8 @@ interface JWTBody {
8291
* sign data. Performs all operations locally, and does not make any RPC calls.
8392
*/
8493
export class ServiceAccountSigner implements CryptoSigner {
94+
95+
algorithm = ALGORITHM_RS256;
8596

8697
/**
8798
* Creates a new CryptoSigner instance from the given service account credential.
@@ -124,6 +135,8 @@ export class ServiceAccountSigner implements CryptoSigner {
124135
* @see https://cloud.google.com/compute/docs/storing-retrieving-metadata
125136
*/
126137
export class IAMSigner implements CryptoSigner {
138+
algorithm = ALGORITHM_RS256;
139+
127140
private readonly httpClient: AuthorizedHttpClient;
128141
private serviceAccountId?: string;
129142

@@ -215,6 +228,29 @@ export class IAMSigner implements CryptoSigner {
215228
}
216229
}
217230

231+
/**
232+
* A CryptoSigner implementation that is used when communicating with the Auth emulator.
233+
* It produces unsigned tokens.
234+
*/
235+
export class EmulatedSigner implements CryptoSigner {
236+
237+
algorithm = ALGORITHM_NONE;
238+
239+
/**
240+
* @inheritDoc
241+
*/
242+
public sign(_: Buffer): Promise<Buffer> {
243+
return Promise.resolve(Buffer.from(''));
244+
}
245+
246+
/**
247+
* @inheritDoc
248+
*/
249+
public getAccountId(): Promise<string> {
250+
return Promise.resolve('[email protected]');
251+
}
252+
}
253+
218254
/**
219255
* Create a new CryptoSigner instance for the given app. If the app has been initialized with a service
220256
* account credential, creates a ServiceAccountSigner. Otherwise creates an IAMSigner.
@@ -250,7 +286,7 @@ export class FirebaseTokenGenerator {
250286
'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.',
251287
);
252288
}
253-
if (typeof tenantId !== 'undefined' && !validator.isNonEmptyString(tenantId)) {
289+
if (typeof this.tenantId !== 'undefined' && !validator.isNonEmptyString(this.tenantId)) {
254290
throw new FirebaseAuthError(
255291
AuthClientErrorCode.INVALID_ARGUMENT,
256292
'`tenantId` argument must be a non-empty string.');
@@ -298,7 +334,7 @@ export class FirebaseTokenGenerator {
298334
}
299335
return this.signer.getAccountId().then((account) => {
300336
const header: JWTHeader = {
301-
alg: ALGORITHM_RS256,
337+
alg: this.signer.algorithm,
302338
typ: 'JWT',
303339
};
304340
const iat = Math.floor(Date.now() / 1000);
@@ -319,6 +355,7 @@ export class FirebaseTokenGenerator {
319355
}
320356
const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`;
321357
const signPromise = this.signer.sign(Buffer.from(token));
358+
322359
return Promise.all([token, signPromise]);
323360
}).then(([token, signature]) => {
324361
return `${token}.${this.encodeSegment(signature)}`;

0 commit comments

Comments
 (0)