diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 753f06e48f..740d173356 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -143,10 +143,9 @@ class AuthResourceUrlBuilder { * @constructor */ constructor(protected app: FirebaseApp, protected version: string = 'v1') { - const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST; - if (emulatorHost) { + if (useEmulator()) { this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { - host: emulatorHost + host: emulatorHost() }); } else { this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; @@ -210,10 +209,9 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { */ constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) { super(app, version); - const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST - if (emulatorHost) { + if (useEmulator()) { this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { - host: emulatorHost + host: emulatorHost() }); } else { this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; @@ -236,6 +234,21 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { } } +/** + * Auth-specific HTTP client which uses the special "owner" token + * when communicating with the Auth Emulator. + */ +class AuthHttpClient extends AuthorizedHttpClient { + + protected getToken(): Promise { + if (useEmulator()) { + return Promise.resolve('owner'); + } + + return super.getToken(); + } + +} /** * Validates an AuthFactorInfo object. All unsupported parameters @@ -991,7 +1004,7 @@ export abstract class AbstractAuthRequestHandler { ); } - this.httpClient = new AuthorizedHttpClient(app); + this.httpClient = new AuthHttpClient(app); } /** @@ -2095,3 +2108,19 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { return super.uploadAccount(users, options); } } + +function emulatorHost(): string | undefined { + return process.env.FIREBASE_AUTH_EMULATOR_HOST +} + +/** + * When true the SDK should communicate with the Auth Emulator for all API + * calls and also produce unsigned tokens. + * + * This alone does NOT short-circuit ID Token verification. + * For security reasons that must be explicitly disabled through + * setJwtVerificationEnabled(false); + */ +export function useEmulator(): boolean { + return !!emulatorHost(); +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 816333e3b2..03d1c4e666 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -21,7 +21,7 @@ import { import { FirebaseApp } from '../firebase-app'; import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator'; import { - AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, + AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator, } from './auth-api-request'; import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; @@ -850,15 +850,3 @@ export class Auth extends BaseAuth 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 NOT 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; -} diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 3b83221d5f..d079b44557 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -814,11 +814,11 @@ export class AuthorizedHttpClient extends HttpClient { } public send(request: HttpRequestConfig): Promise { - return this.app.INTERNAL.getToken().then((accessTokenObj) => { + return this.getToken().then((token) => { const requestCopy = Object.assign({}, request); requestCopy.headers = Object.assign({}, request.headers); const authHeader = 'Authorization'; - requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`; + requestCopy.headers[authHeader] = `Bearer ${token}`; if (!requestCopy.httpAgent && this.app.options.httpAgent) { requestCopy.httpAgent = this.app.options.httpAgent; @@ -826,6 +826,13 @@ export class AuthorizedHttpClient extends HttpClient { return super.send(requestCopy); }); } + + protected getToken(): Promise { + return this.app.INTERNAL.getToken() + .then((accessTokenObj) => { + return accessTokenObj.accessToken; + }); + } } /** diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 0a86231e53..13d5fcfc33 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -865,6 +865,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; + const expectedHeadersEmulator: {[key: string]: string} = { + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, + 'Authorization': 'Bearer owner', + }; const callParams = (path: string, method: any, data: any): HttpRequestConfig => { return { method, @@ -902,6 +906,59 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + describe('Emulator Support', () => { + const method = 'POST'; + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const expectedResult = utils.responseFrom({ + users : [ + { localId: 'uid' }, + ], + }); + const data = { localId: ['uid'] }; + + after(() => { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + }) + + it('should call a prod URL with a real token when emulator is not running', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `https://${host}${path}`, + data, + headers: expectedHeaders, + timeout, + }); + }); + }); + + it('should call a local URL with a mock token when the emulator is running', () => { + 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); + 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, + }); + }); + }); + }); + describe('createSessionCookie', () => { const durationInMs = 24 * 60 * 60 * 1000; const path = handler.path('v1', ':createSessionCookie', 'project_id');