Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Admin AuthTokenProvider when targeting Emulator #3228

Merged
merged 11 commits into from
Jul 14, 2020
8 changes: 8 additions & 0 deletions .changeset/smooth-poets-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"firebase": patch
"@firebase/database": patch
---

[fix] Instead of using production auth, the SDK will use test credentials
to connect to the Emulator when the RTDB SDK is used via the Firebase
Admin SDK.
36 changes: 32 additions & 4 deletions packages/database/src/core/AuthTokenProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ import { Provider } from '@firebase/component';
import { log, warn } from './util/util';
import { FirebaseApp } from '@firebase/app-types';

export interface AuthTokenProvider {
getToken(forceRefresh: boolean): Promise<FirebaseAuthTokenData>;
addTokenChangeListener(listener: (token: string | null) => void): void;
removeTokenChangeListener(listener: (token: string | null) => void): void;
notifyForInvalidToken(): void;
}

/**
* Abstraction around FirebaseApp's token fetching capabilities.
*/
export class AuthTokenProvider {
export class FirebaseAuthTokenProvider implements AuthTokenProvider {
private auth_: FirebaseAuthInternal | null = null;
constructor(
private app_: FirebaseApp,
Expand Down Expand Up @@ -60,7 +67,7 @@ export class AuthTokenProvider {
});
}

addTokenChangeListener(listener: (token: string | null) => void) {
addTokenChangeListener(listener: (token: string | null) => void): void {
// TODO: We might want to wrap the listener and call it with no args to
// avoid a leaky abstraction, but that makes removing the listener harder.
if (this.auth_) {
Expand All @@ -73,13 +80,13 @@ export class AuthTokenProvider {
}
}

removeTokenChangeListener(listener: (token: string | null) => void) {
removeTokenChangeListener(listener: (token: string | null) => void): void {
this.authProvider_
.get()
.then(auth => auth.removeAuthTokenListener(listener));
}

notifyForInvalidToken() {
notifyForInvalidToken(): void {
let errorMessage =
'Provided authentication credentials for the app named "' +
this.app_.name +
Expand All @@ -104,3 +111,24 @@ export class AuthTokenProvider {
warn(errorMessage);
}
}

/* Auth token provider that the Admin SDK uses to connect to the Emulator. */
export class EmulatorAdminTokenProvider implements AuthTokenProvider {
private static EMULATOR_AUTH_TOKEN = 'owner';

getToken(forceRefresh: boolean): Promise<FirebaseAuthTokenData> {
return Promise.resolve({
accessToken: EmulatorAdminTokenProvider.EMULATOR_AUTH_TOKEN
});
}

addTokenChangeListener(listener: (token: string | null) => void): void {
// Invoke the listener immediately to match the behavior in Firebase Auth
// (see packages/auth/src/auth.js#L1807)
listener(EmulatorAdminTokenProvider.EMULATOR_AUTH_TOKEN);
}

removeTokenChangeListener(listener: (token: string | null) => void): void {}

notifyForInvalidToken(): void {}
}
4 changes: 1 addition & 3 deletions packages/database/src/core/Repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,8 @@ export class Repo {
public repoInfo_: RepoInfo,
forceRestClient: boolean,
public app: FirebaseApp,
authProvider: Provider<FirebaseAuthInternalName>
authTokenProvider: AuthTokenProvider
) {
const authTokenProvider = new AuthTokenProvider(app, authProvider);

this.stats_ = StatsManager.getCollection(repoInfo_);

if (forceRestClient || beingCrawled()) {
Expand Down
24 changes: 20 additions & 4 deletions packages/database/src/core/RepoManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { FirebaseApp } from '@firebase/app-types';
import { safeGet } from '@firebase/util';
import { safeGet, CONSTANTS } from '@firebase/util';
import { Repo } from './Repo';
import { fatal } from './util/util';
import { parseRepoInfo } from './util/libs/parser';
Expand All @@ -26,6 +26,11 @@ import { Database } from '../api/Database';
import { RepoInfo } from './RepoInfo';
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
import { Provider } from '@firebase/component';
import {
AuthTokenProvider,
EmulatorAdminTokenProvider,
FirebaseAuthTokenProvider
} from './AuthTokenProvider';

/** @const {string} */
const DATABASE_URL_OPTION = 'databaseURL';
Expand Down Expand Up @@ -108,16 +113,27 @@ export class RepoManager {
let parsedUrl = parseRepoInfo(dbUrl);
let repoInfo = parsedUrl.repoInfo;

let isEmulator: boolean;

let dbEmulatorHost: string | undefined = undefined;
if (typeof process !== 'undefined') {
dbEmulatorHost = process.env[FIREBASE_DATABASE_EMULATOR_HOST_VAR];
}

if (dbEmulatorHost) {
isEmulator = true;
dbUrl = `http://${dbEmulatorHost}?ns=${repoInfo.namespace}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this branch if (dbEmulatorHost) { } trigger only if the emulator connection is made via env var? What about if the user does this:

firebase.initializeApp({
  databaseURL: 'http://localhost:9000?ns=namespace'
})

You might consider using the EmulatorAdminTokenProvider any time Constants.NODE_ADMIN is true and the database is http:// (not https://). That's what we do in the Firebase CLI, it catches more cases and it also has the nice side benefit of not allowing someone's admin credentials to ever go to an insecure host.

parsedUrl = parseRepoInfo(dbUrl);
repoInfo = parsedUrl.repoInfo;
} else {
isEmulator = !parsedUrl.repoInfo.secure;
}

const authTokenProvider =
CONSTANTS.NODE_ADMIN && isEmulator
? new EmulatorAdminTokenProvider()
: new FirebaseAuthTokenProvider(app, authProvider);

validateUrl('Invalid Firebase Database URL', 1, parsedUrl);
if (!parsedUrl.path.isEmpty()) {
fatal(
Expand All @@ -126,7 +142,7 @@ export class RepoManager {
);
}

const repo = this.createRepo(repoInfo, app, authProvider);
const repo = this.createRepo(repoInfo, app, authTokenProvider);

return repo.database;
}
Expand Down Expand Up @@ -159,7 +175,7 @@ export class RepoManager {
createRepo(
repoInfo: RepoInfo,
app: FirebaseApp,
authProvider: Provider<FirebaseAuthInternalName>
authTokenProvider: AuthTokenProvider
): Repo {
let appRepos = safeGet(this.repos_, app.name);

Expand All @@ -174,7 +190,7 @@ export class RepoManager {
'Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.'
);
}
repo = new Repo(repoInfo, this.useRestClient_, app, authProvider);
repo = new Repo(repoInfo, this.useRestClient_, app, authTokenProvider);
appRepos[repoInfo.toURLString()] = repo;

return repo;
Expand Down