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

Add mockUserToken support for database emulator. #4792

Merged
merged 22 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4f7f840
Add mockUserToken support for database emulator.
yuchenshi Apr 15, 2021
21374c2
Add compact API.
yuchenshi Apr 15, 2021
5fd82c7
Add generated API file.
yuchenshi Apr 15, 2021
4d8b954
Change default project ID to demo-project.
yuchenshi Apr 20, 2021
e5dae20
Merge remote-tracking branch 'origin/master' into ys/mockUserToken-da…
yuchenshi Apr 22, 2021
6bdd4e8
Merge remote-tracking branch 'origin/master' into ys/mockUserToken-da…
yuchenshi Apr 22, 2021
9f0b203
Fix db.useEmulator
schmidt-sebastian Apr 23, 2021
a829398
Create sweet-monkeys-warn.md
schmidt-sebastian Apr 23, 2021
efe6d17
Update packages/database/src/exp/Database.ts
yuchenshi Apr 23, 2021
f0b11f0
Update packages/util/test/emulator.test.ts
yuchenshi Apr 23, 2021
82cd88e
Merge remote-tracking branch 'origin/mrschmidt/emulator' into ys/mock…
yuchenshi Apr 24, 2021
ff24362
Fix sub field name.
yuchenshi Apr 24, 2021
05c383e
Remove optional in jsdocs.
yuchenshi Apr 24, 2021
9d99437
Create loud-feet-jump.md
yuchenshi Apr 24, 2021
f3aebe0
Update loud-feet-jump.md
yuchenshi Apr 24, 2021
a684219
Make sub/user_id required in typing.
yuchenshi Apr 27, 2021
6238b8b
Update error messages to contain mockUserToken.
yuchenshi Apr 27, 2021
2059715
Merge branch 'master' into ys/mockUserToken-database
yuchenshi Apr 27, 2021
3a1cd98
Add API changes in md.
yuchenshi Apr 27, 2021
ea53f0c
Update error message for uid field.
yuchenshi Apr 28, 2021
c0b51f8
Update loud-feet-jump.md
yuchenshi Apr 28, 2021
dc72577
Change custom claim typing to unknown.
yuchenshi Apr 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/loud-feet-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@firebase/database": minor
"firebase": minor
"@firebase/util": minor
---

Add mockUserToken support for database emulator.
7 changes: 5 additions & 2 deletions common/api-review/database.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

```ts

import { EmulatorMockTokenOptions } from '@firebase/util';
import { FirebaseApp } from '@firebase/app';

// @public (undocumented)
// @public
export function child(parent: Reference, path: string): Reference;

// @public
Expand Down Expand Up @@ -229,7 +230,9 @@ export type Unsubscribe = () => void;
export function update(ref: Reference, values: object): Promise<void>;

// @public
export function useDatabaseEmulator(db: FirebaseDatabase, host: string, port: number): void;
export function useDatabaseEmulator(db: FirebaseDatabase, host: string, port: number, options?: {
mockUserToken?: EmulatorMockTokenOptions;
}): void;


```
17 changes: 14 additions & 3 deletions packages/database/src/api/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@

import { FirebaseApp } from '@firebase/app-types';
import { FirebaseService } from '@firebase/app-types/private';
import { validateArgCount, Compat } from '@firebase/util';
import {
validateArgCount,
Compat,
EmulatorMockTokenOptions
} from '@firebase/util';

import {
FirebaseDatabase as ExpDatabase,
Expand Down Expand Up @@ -58,9 +62,16 @@ export class Database implements FirebaseService, Compat<ExpDatabase> {
*
* @param host - the emulator host (ex: localhost)
* @param port - the emulator port (ex: 8080)
* @param options.mockUserToken - the mock auth token to use for unit testing Security Rules
*/
useEmulator(host: string, port: number): void {
useDatabaseEmulator(this._delegate, host, port);
useEmulator(
host: string,
port: number,
options: {
mockUserToken?: EmulatorMockTokenOptions;
} = {}
): void {
useDatabaseEmulator(this._delegate, host, port, options);
}

/**
Expand Down
13 changes: 8 additions & 5 deletions packages/database/src/core/AuthTokenProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,23 @@ export class FirebaseAuthTokenProvider implements AuthTokenProvider {
}
}

/* Auth token provider that the Admin SDK uses to connect to the Emulator. */
export class EmulatorAdminTokenProvider implements AuthTokenProvider {
private static EMULATOR_AUTH_TOKEN = 'owner';
/* AuthTokenProvider that supplies a constant token. Used by Admin SDK or mockUserToken with emulators. */
export class EmulatorTokenProvider implements AuthTokenProvider {
/** A string that is treated as an admin access token by the RTDB emulator. Used by Admin SDK. */
static OWNER = 'owner';

constructor(private accessToken: string) {}

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

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);
listener(this.accessToken);
}

removeTokenChangeListener(listener: (token: string | null) => void): void {}
Expand Down
43 changes: 35 additions & 8 deletions packages/database/src/exp/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ import {
} from '@firebase/app-exp';
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
import { Provider } from '@firebase/component';
import { getModularInstance } from '@firebase/util';
import {
getModularInstance,
createMockUserToken,
EmulatorMockTokenOptions
} from '@firebase/util';

import {
AuthTokenProvider,
EmulatorAdminTokenProvider,
EmulatorTokenProvider,
FirebaseAuthTokenProvider
} from '../core/AuthTokenProvider';
import { Repo, repoInterrupt, repoResume, repoStart } from '../core/Repo';
Expand Down Expand Up @@ -74,7 +78,8 @@ let useRestClient = false;
function repoManagerApplyEmulatorSettings(
repo: Repo,
host: string,
port: number
port: number,
tokenProvider?: AuthTokenProvider
): void {
repo.repoInfo_ = new RepoInfo(
`${host}:${port}`,
Expand All @@ -86,8 +91,8 @@ function repoManagerApplyEmulatorSettings(
repo.repoInfo_.includeNamespaceInQueryParams
);

if (repo.repoInfo_.nodeAdmin) {
repo.authTokenProvider_ = new EmulatorAdminTokenProvider();
if (tokenProvider) {
repo.authTokenProvider_ = tokenProvider;
}
}

Expand Down Expand Up @@ -135,7 +140,7 @@ export function repoManagerDatabaseFromApp(

const authTokenProvider =
nodeAdmin && isEmulator
? new EmulatorAdminTokenProvider()
? new EmulatorTokenProvider(EmulatorTokenProvider.OWNER)
: new FirebaseAuthTokenProvider(app.name, app.options, authProvider);

validateUrl('Invalid Firebase Database URL', parsedUrl);
Expand Down Expand Up @@ -286,11 +291,15 @@ export function getDatabase(
* @param db - The instance to modify.
* @param host - The emulator host (ex: localhost)
* @param port - The emulator port (ex: 8080)
* @param options.mockUserToken - the mock auth token to use for unit testing Security Rules
*/
export function useDatabaseEmulator(
db: FirebaseDatabase,
host: string,
port: number
port: number,
options: {
mockUserToken?: EmulatorMockTokenOptions;
} = {}
): void {
db = getModularInstance(db);
db._checkNotDeleted('useEmulator');
Expand All @@ -299,8 +308,26 @@ export function useDatabaseEmulator(
'Cannot call useEmulator() after instance has already been initialized.'
);
}

const repo = db._repoInternal;
let tokenProvider: EmulatorTokenProvider | undefined = undefined;
if (repo.repoInfo_.nodeAdmin) {
if (options.mockUserToken) {
fatal(
'mockUserToken is not supported by the Admin SDK. For client access with mock users, please use the "firebase" package instead of "firebase-admin".'
);
}
tokenProvider = new EmulatorTokenProvider(EmulatorTokenProvider.OWNER);
} else if (options.mockUserToken) {
const token = createMockUserToken(
options.mockUserToken,
db.app.options.projectId
);
tokenProvider = new EmulatorTokenProvider(token);
}

// Modify the repo to apply emulator settings
repoManagerApplyEmulatorSettings(db._repoInternal, host, port);
repoManagerApplyEmulatorSettings(repo, host, port, tokenProvider);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/util/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './src/crypt';
export * from './src/constants';
export * from './src/deepCopy';
export * from './src/deferred';
export * from './src/emulator';
export * from './src/environment';
export * from './src/errors';
export * from './src/json';
Expand Down
1 change: 1 addition & 0 deletions packages/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './src/crypt';
export * from './src/constants';
export * from './src/deepCopy';
export * from './src/deferred';
export * from './src/emulator';
export * from './src/environment';
export * from './src/errors';
export * from './src/json';
Expand Down
142 changes: 142 additions & 0 deletions packages/util/src/emulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { base64 } from './crypt';

// Firebase Auth tokens contain snake_case claims following the JWT standard / convention.
/* eslint-disable camelcase */

export type FirebaseSignInProvider =
| 'custom'
| 'email'
| 'password'
| 'phone'
| 'anonymous'
| 'google.com'
| 'facebook.com'
| 'github.com'
| 'twitter.com'
| 'microsoft.com'
| 'apple.com';

interface FirebaseIdToken {
// Always set to https://securetoken.google.com/PROJECT_ID
iss: string;

// Always set to PROJECT_ID
aud: string;

// The user's unique id
sub: string;

// The token issue time, in seconds since epoch
iat: number;

// The token expiry time, normally 'iat' + 3600
exp: number;

// The user's unique id, must be equal to 'sub'
user_id: string;

// The time the user authenticated, normally 'iat'
auth_time: number;

// The sign in provider, only set when the provider is 'anonymous'
provider_id?: 'anonymous';

// The user's primary email
email?: string;

// The user's email verification status
email_verified?: boolean;

// The user's primary phone number
phone_number?: string;

// The user's display name
name?: string;

// The user's profile photo URL
picture?: string;

// Information on all identities linked to this user
firebase: {
// The primary sign-in provider
sign_in_provider: FirebaseSignInProvider;

// A map of providers to the user's list of unique identifiers from
// each provider
identities?: { [provider in FirebaseSignInProvider]?: string[] };
};

// Custom claims set by the developer
[claim: string]: unknown;

uid?: never; // Try to catch a common mistake of "uid" (should be "sub" instead).
}

export type EmulatorMockTokenOptions = ({ user_id: string } | { sub: string }) &
Partial<FirebaseIdToken>;

export function createMockUserToken(
yuchenshi marked this conversation as resolved.
Show resolved Hide resolved
token: EmulatorMockTokenOptions,
projectId?: string
): string {
if (token.uid) {
yuchenshi marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(
'The "uid" field is no longer supported by mockUserToken. Please use "sub" instead for Firebase Auth User ID.'
);
}
// Unsecured JWTs use "none" as the algorithm.
const header = {
alg: 'none',
type: 'JWT'
};

const project = projectId || 'demo-project';
const iat = token.iat || 0;
const sub = token.sub || token.user_id;
if (!sub) {
throw new Error("mockUserToken must contain 'sub' or 'user_id' field!");
}

const payload: FirebaseIdToken = {
// Set all required fields to decent defaults
iss: `https://securetoken.google.com/${project}`,
aud: project,
iat,
exp: iat + 3600,
auth_time: iat,
sub,
user_id: sub,
firebase: {
sign_in_provider: 'custom',
identities: {}
},

// Override with user options
...token
};

// Unsecured JWTs use the empty string as a signature.
const signature = '';
return [
base64.encodeString(JSON.stringify(header), /*webSafe=*/ false),
base64.encodeString(JSON.stringify(payload), /*webSafe=*/ false),
signature
].join('.');
}