Skip to content
Open
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
53 changes: 34 additions & 19 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,15 @@ class AuthResourceUrlBuilder {
/**
* The resource URL builder constructor.
*
* @param projectId - The resource project ID.
* @param app - The app for this URL builder.
* @param version - The endpoint API version.
* @param emulatorHost - Optional emulator host captured at init time.
* @constructor
*/
constructor(protected app: App, protected version: string = 'v1') {
if (useEmulator()) {
constructor(protected app: App, protected version: string = 'v1', emHost?: string) {
if (emHost) {
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, {
host: emulatorHost()
host: emHost
});
} else {
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
Expand Down Expand Up @@ -190,16 +191,17 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
/**
* The tenant aware resource URL builder constructor.
*
* @param projectId - The resource project ID.
* @param app - The app for this URL builder.
* @param version - The endpoint API version.
* @param tenantId - The tenant ID.
* @param emHost - Optional emulator host captured at init time.
* @constructor
*/
constructor(protected app: App, protected version: string, protected tenantId: string) {
super(app, version);
if (useEmulator()) {
constructor(protected app: App, protected version: string, protected tenantId: string, emHost?: string) {
super(app, version, emHost);
if (emHost) {
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, {
host: emulatorHost()
host: emHost
});
} else {
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
Expand Down Expand Up @@ -228,8 +230,15 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
*/
class AuthHttpClient extends AuthorizedHttpClient {

private readonly isEmulator: boolean;

constructor(app: FirebaseApp, isEmulator: boolean) {
super(app);
this.isEmulator = isEmulator;
}

protected getToken(): Promise<string> {
if (useEmulator()) {
if (this.isEmulator) {
return Promise.resolve('owner');
}

Expand Down Expand Up @@ -1013,6 +1022,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET')
export abstract class AbstractAuthRequestHandler {

protected readonly httpClient: AuthorizedHttpClient;
public readonly emulatorHostValue: string | undefined;
private authUrlBuilder: AuthResourceUrlBuilder;
private projectConfigUrlBuilder: AuthResourceUrlBuilder;

Expand Down Expand Up @@ -1067,17 +1077,21 @@ export abstract class AbstractAuthRequestHandler {

/**
* @param app - The app used to fetch access tokens to sign API requests.
* @param emHost - Optional emulator host override. When provided (including
* null for explicitly no emulator), this value is used instead of reading
* from the FIREBASE_AUTH_EMULATOR_HOST environment variable.
* @constructor
*/
constructor(protected readonly app: App) {
constructor(protected readonly app: App, emHost?: string | null) {
if (typeof app !== 'object' || app === null || !('options' in app)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'First argument passed to admin.auth() must be a valid Firebase app instance.',
);
}

this.httpClient = new AuthHttpClient(app as FirebaseApp);
this.emulatorHostValue = emHost !== undefined ? (emHost || undefined) : emulatorHost();
this.httpClient = new AuthHttpClient(app as FirebaseApp, !!this.emulatorHostValue);
}

/**
Expand Down Expand Up @@ -2088,21 +2102,21 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
*/
constructor(app: App) {
super(app);
this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2');
this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2', this.emulatorHostValue);
}

/**
* @returns A new Auth user management resource URL builder instance.
*/
protected newAuthUrlBuilder(): AuthResourceUrlBuilder {
return new AuthResourceUrlBuilder(this.app, 'v1');
return new AuthResourceUrlBuilder(this.app, 'v1', this.emulatorHostValue);
}

/**
* @returns A new project config resource URL builder instance.
*/
protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
return new AuthResourceUrlBuilder(this.app, 'v2');
return new AuthResourceUrlBuilder(this.app, 'v2', this.emulatorHostValue);
}

/**
Expand Down Expand Up @@ -2259,24 +2273,25 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler {
*
* @param app - The app used to fetch access tokens to sign API requests.
* @param tenantId - The request handler's tenant ID.
* @param emHost - Optional emulator host override captured at init time.
* @constructor
*/
constructor(app: App, private readonly tenantId: string) {
super(app);
constructor(app: App, private readonly tenantId: string, emHost?: string | null) {
super(app, emHost);
}

/**
* @returns A new Auth user management resource URL builder instance.
*/
protected newAuthUrlBuilder(): AuthResourceUrlBuilder {
return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId);
return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId, this.emulatorHostValue);
}

/**
* @returns A new project config resource URL builder instance.
*/
protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder {
return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2', this.tenantId);
return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2', this.tenantId, this.emulatorHostValue);
}

/**
Expand Down
16 changes: 10 additions & 6 deletions src/auth/base-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,10 @@ export interface SessionCookieOptions {
* @internal
*/
export function createFirebaseTokenGenerator(app: App,
tenantId?: string): FirebaseTokenGenerator {
tenantId?: string, isEmulator?: boolean): FirebaseTokenGenerator {
try {
const signer = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app);
const shouldEmulate = isEmulator !== undefined ? isEmulator : useEmulator();
const signer = shouldEmulate ? new EmulatedSigner() : cryptoSignerFromApp(app);
return new FirebaseTokenGenerator(signer, tenantId);
} catch (err) {
throw handleCryptoSignerError(err);
Expand All @@ -138,6 +139,7 @@ export abstract class BaseAuth {
protected readonly authBlockingTokenVerifier: FirebaseTokenVerifier;
/** @internal */
protected readonly sessionCookieVerifier: FirebaseTokenVerifier;
private readonly emulatorMode: boolean;

/**
* The BaseAuth class constructor.
Expand All @@ -154,6 +156,8 @@ export abstract class BaseAuth {
app: App,
/** @internal */ protected readonly authRequestHandler: AbstractAuthRequestHandler,
tokenGenerator?: FirebaseTokenGenerator) {
this.emulatorMode = !!this.authRequestHandler.emulatorHostValue;

if (tokenGenerator) {
this.tokenGenerator = tokenGenerator;
} else {
Expand Down Expand Up @@ -210,7 +214,7 @@ export abstract class BaseAuth {
* promise.
*/
public verifyIdToken(idToken: string, checkRevoked = false): Promise<DecodedIdToken> {
const isEmulator = useEmulator();
const isEmulator = this.emulatorMode;
return this.idTokenVerifier.verifyJWT(idToken, isEmulator)
.then((decodedIdToken: DecodedIdToken) => {
// Whether to check if the token was revoked.
Expand Down Expand Up @@ -716,7 +720,7 @@ export abstract class BaseAuth {
*/
public verifySessionCookie(
sessionCookie: string, checkRevoked = false): Promise<DecodedIdToken> {
const isEmulator = useEmulator();
const isEmulator = this.emulatorMode;
return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator)
.then((decodedIdToken: DecodedIdToken) => {
// Whether to check if the token was revoked.
Expand Down Expand Up @@ -1094,10 +1098,10 @@ export abstract class BaseAuth {
/** @alpha */
// eslint-disable-next-line @typescript-eslint/naming-convention
public _verifyAuthBlockingToken(
token: string,
token: string,
audience?: string
): Promise<DecodedAuthBlockingToken> {
const isEmulator = useEmulator();
const isEmulator = this.emulatorMode;
return this.authBlockingTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience)
.then((decodedAuthBlockingToken: DecodedAuthBlockingToken) => {
return decodedAuthBlockingToken;
Expand Down
11 changes: 8 additions & 3 deletions src/auth/tenant-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,15 @@ export class TenantAwareAuth extends BaseAuth {
*
* @param app - The app that created this tenant.
* @param tenantId - The corresponding tenant ID.
* @param emHost - Optional emulator host captured at init time.
* @constructor
* @internal
*/
constructor(app: App, tenantId: string) {
constructor(app: App, tenantId: string, emHost?: string | null) {
const emIsSet = emHost !== undefined;
super(app, new TenantAwareAuthRequestHandler(
app, tenantId), createFirebaseTokenGenerator(app, tenantId));
app, tenantId, emHost), createFirebaseTokenGenerator(
app, tenantId, emIsSet ? !!emHost : undefined));
utils.addReadonlyGetter(this, 'tenantId', tenantId);
}

Expand Down Expand Up @@ -148,6 +151,7 @@ export class TenantAwareAuth extends BaseAuth {
export class TenantManager {
private readonly authRequestHandler: AuthRequestHandler;
private readonly tenantsMap: {[key: string]: TenantAwareAuth};
private readonly emulatorHost: string | undefined;

/**
* Initializes a TenantManager instance for a specified FirebaseApp.
Expand All @@ -159,6 +163,7 @@ export class TenantManager {
*/
constructor(private readonly app: App) {
this.authRequestHandler = new AuthRequestHandler(app);
this.emulatorHost = this.authRequestHandler.emulatorHostValue;
this.tenantsMap = {};
}

Expand All @@ -174,7 +179,7 @@ export class TenantManager {
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID);
}
if (typeof this.tenantsMap[tenantId] === 'undefined') {
this.tenantsMap[tenantId] = new TenantAwareAuth(this.app, tenantId);
this.tenantsMap[tenantId] = new TenantAwareAuth(this.app, tenantId, this.emulatorHost ?? null);
}
return this.tenantsMap[tenantId];
}
Expand Down
44 changes: 44 additions & 0 deletions test/unit/auth/auth-api-request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,50 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => {
});
});
});

it('should keep using emulator after env var is deleted', () => {
const emulatorHost = 'localhost:9099';
process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost;

const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
stubs.push(stub);

const requestHandler = handler.init(mockApp);
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;

return requestHandler.getAccountInfoByUid('uid')
.then(() => {
expect(stub).to.have.been.calledOnce.and.calledWith({
method,
url: `http://${emulatorHost}/identitytoolkit.googleapis.com${path}`,
data,
headers: expectedHeadersEmulator,
timeout,
});
});
});

it('should not use emulator when env var is set after initialization', () => {
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;

const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
stubs.push(stub);

const requestHandler = handler.init(mockApp);
process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099';

return requestHandler.getAccountInfoByUid('uid')
.then(() => {
expect(stub).to.have.been.calledOnce.and.calledWith({
method,
url: `https://${host}${path}`,
data,
headers: expectedHeaders,
timeout,
});
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;
});
});
});

describe('createSessionCookie', () => {
Expand Down
Loading
Loading