Skip to content

Commit cb3d2c3

Browse files
authored
Use 'owner' token when communicating with Auth emulator (#1085)
* Use 'owner' token when communicating with Auth emulator * Unused import * Single quote * Simplify to address code review * Further simplify * Reduce diff * Stray comma * Remove circular import, add unit test * Final review feedback
1 parent fb2c63c commit cb3d2c3

File tree

4 files changed

+103
-22
lines changed

4 files changed

+103
-22
lines changed

src/auth/auth-api-request.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,9 @@ class AuthResourceUrlBuilder {
143143
* @constructor
144144
*/
145145
constructor(protected app: FirebaseApp, protected version: string = 'v1') {
146-
const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST;
147-
if (emulatorHost) {
146+
if (useEmulator()) {
148147
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, {
149-
host: emulatorHost
148+
host: emulatorHost()
150149
});
151150
} else {
152151
this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;
@@ -210,10 +209,9 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
210209
*/
211210
constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) {
212211
super(app, version);
213-
const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST
214-
if (emulatorHost) {
212+
if (useEmulator()) {
215213
this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, {
216-
host: emulatorHost
214+
host: emulatorHost()
217215
});
218216
} else {
219217
this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT;
@@ -236,6 +234,21 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
236234
}
237235
}
238236

237+
/**
238+
* Auth-specific HTTP client which uses the special "owner" token
239+
* when communicating with the Auth Emulator.
240+
*/
241+
class AuthHttpClient extends AuthorizedHttpClient {
242+
243+
protected getToken(): Promise<string> {
244+
if (useEmulator()) {
245+
return Promise.resolve('owner');
246+
}
247+
248+
return super.getToken();
249+
}
250+
251+
}
239252

240253
/**
241254
* Validates an AuthFactorInfo object. All unsupported parameters
@@ -991,7 +1004,7 @@ export abstract class AbstractAuthRequestHandler {
9911004
);
9921005
}
9931006

994-
this.httpClient = new AuthorizedHttpClient(app);
1007+
this.httpClient = new AuthHttpClient(app);
9951008
}
9961009

9971010
/**
@@ -2095,3 +2108,19 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler {
20952108
return super.uploadAccount(users, options);
20962109
}
20972110
}
2111+
2112+
function emulatorHost(): string | undefined {
2113+
return process.env.FIREBASE_AUTH_EMULATOR_HOST
2114+
}
2115+
2116+
/**
2117+
* When true the SDK should communicate with the Auth Emulator for all API
2118+
* calls and also produce unsigned tokens.
2119+
*
2120+
* This alone does <b>NOT<b> short-circuit ID Token verification.
2121+
* For security reasons that must be explicitly disabled through
2122+
* setJwtVerificationEnabled(false);
2123+
*/
2124+
export function useEmulator(): boolean {
2125+
return !!emulatorHost();
2126+
}

src/auth/auth.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { FirebaseApp } from '../firebase-app';
2222
import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator';
2323
import {
24-
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler,
24+
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator,
2525
} from './auth-api-request';
2626
import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error';
2727
import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service';
@@ -850,15 +850,3 @@ export class Auth extends BaseAuth<AuthRequestHandler>
850850
return this.tenantManager_;
851851
}
852852
}
853-
854-
/**
855-
* When true the SDK should communicate with the Auth Emulator for all API
856-
* calls and also produce unsigned tokens.
857-
*
858-
* This alone does <b>NOT<b> short-circuit ID Token verification.
859-
* For security reasons that must be explicitly disabled through
860-
* setJwtVerificationEnabled(false);
861-
*/
862-
function useEmulator(): boolean {
863-
return !!process.env.FIREBASE_AUTH_EMULATOR_HOST;
864-
}

src/utils/api-request.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,18 +814,25 @@ export class AuthorizedHttpClient extends HttpClient {
814814
}
815815

816816
public send(request: HttpRequestConfig): Promise<HttpResponse> {
817-
return this.app.INTERNAL.getToken().then((accessTokenObj) => {
817+
return this.getToken().then((token) => {
818818
const requestCopy = Object.assign({}, request);
819819
requestCopy.headers = Object.assign({}, request.headers);
820820
const authHeader = 'Authorization';
821-
requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`;
821+
requestCopy.headers[authHeader] = `Bearer ${token}`;
822822

823823
if (!requestCopy.httpAgent && this.app.options.httpAgent) {
824824
requestCopy.httpAgent = this.app.options.httpAgent;
825825
}
826826
return super.send(requestCopy);
827827
});
828828
}
829+
830+
protected getToken(): Promise<string> {
831+
return this.app.INTERNAL.getToken()
832+
.then((accessTokenObj) => {
833+
return accessTokenObj.accessToken;
834+
});
835+
}
829836
}
830837

831838
/**

test/unit/auth/auth-api-request.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => {
865865
'X-Client-Version': `Node/Admin/${getSdkVersion()}`,
866866
'Authorization': 'Bearer ' + mockAccessToken,
867867
};
868+
const expectedHeadersEmulator: {[key: string]: string} = {
869+
'X-Client-Version': `Node/Admin/${getSdkVersion()}`,
870+
'Authorization': 'Bearer owner',
871+
};
868872
const callParams = (path: string, method: any, data: any): HttpRequestConfig => {
869873
return {
870874
method,
@@ -902,6 +906,59 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => {
902906
});
903907
});
904908

909+
describe('Emulator Support', () => {
910+
const method = 'POST';
911+
const path = handler.path('v1', '/accounts:lookup', 'project_id');
912+
const expectedResult = utils.responseFrom({
913+
users : [
914+
{ localId: 'uid' },
915+
],
916+
});
917+
const data = { localId: ['uid'] };
918+
919+
after(() => {
920+
delete process.env.FIREBASE_AUTH_EMULATOR_HOST;
921+
})
922+
923+
it('should call a prod URL with a real token when emulator is not running', () => {
924+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
925+
stubs.push(stub);
926+
927+
const requestHandler = handler.init(mockApp);
928+
929+
return requestHandler.getAccountInfoByUid('uid')
930+
.then(() => {
931+
expect(stub).to.have.been.calledOnce.and.calledWith({
932+
method,
933+
url: `https://${host}${path}`,
934+
data,
935+
headers: expectedHeaders,
936+
timeout,
937+
});
938+
});
939+
});
940+
941+
it('should call a local URL with a mock token when the emulator is running', () => {
942+
const emulatorHost = 'localhost:9099';
943+
process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost;
944+
945+
const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult);
946+
stubs.push(stub);
947+
948+
const requestHandler = handler.init(mockApp);
949+
return requestHandler.getAccountInfoByUid('uid')
950+
.then(() => {
951+
expect(stub).to.have.been.calledOnce.and.calledWith({
952+
method,
953+
url: `http://${emulatorHost}/identitytoolkit.googleapis.com${path}`,
954+
data,
955+
headers: expectedHeadersEmulator,
956+
timeout,
957+
});
958+
});
959+
});
960+
});
961+
905962
describe('createSessionCookie', () => {
906963
const durationInMs = 24 * 60 * 60 * 1000;
907964
const path = handler.path('v1', ':createSessionCookie', 'project_id');

0 commit comments

Comments
 (0)