Skip to content

Commit

Permalink
Use 'owner' token when communicating with Auth emulator (#1085)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
samtstern committed Nov 11, 2020
1 parent fb2c63c commit cb3d2c3
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 22 deletions.
43 changes: 36 additions & 7 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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<string> {
if (useEmulator()) {
return Promise.resolve('owner');
}

return super.getToken();
}

}

/**
* Validates an AuthFactorInfo object. All unsupported parameters
Expand Down Expand Up @@ -991,7 +1004,7 @@ export abstract class AbstractAuthRequestHandler {
);
}

this.httpClient = new AuthorizedHttpClient(app);
this.httpClient = new AuthHttpClient(app);
}

/**
Expand Down Expand Up @@ -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 <b>NOT<b> short-circuit ID Token verification.
* For security reasons that must be explicitly disabled through
* setJwtVerificationEnabled(false);
*/
export function useEmulator(): boolean {
return !!emulatorHost();
}
14 changes: 1 addition & 13 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -850,15 +850,3 @@ export class Auth extends BaseAuth<AuthRequestHandler>
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;
}
11 changes: 9 additions & 2 deletions src/utils/api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,18 +814,25 @@ export class AuthorizedHttpClient extends HttpClient {
}

public send(request: HttpRequestConfig): Promise<HttpResponse> {
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;
}
return super.send(requestCopy);
});
}

protected getToken(): Promise<string> {
return this.app.INTERNAL.getToken()
.then((accessTokenObj) => {
return accessTokenObj.accessToken;
});
}
}

/**
Expand Down
57 changes: 57 additions & 0 deletions test/unit/auth/auth-api-request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit cb3d2c3

Please sign in to comment.