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

Improve typing for options.auth #3876

Merged
merged 2 commits into from
Oct 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-ways-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/rules-unit-testing': patch
---

Add stronger types to the 'options.auth' option for initializeTestApp
125 changes: 109 additions & 16 deletions packages/rules-unit-testing/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,118 @@ let _databaseHost: string | undefined = undefined;
/** The actual address for the Firestore emulator */
let _firestoreHost: string | undefined = undefined;

/** Create an unsecured JWT for the given auth payload. See https://tools.ietf.org/html/rfc7519#section-6. */
function createUnsecuredJwt(auth: object): string {
export type Provider =
| 'custom'
| 'email'
| 'password'
| 'phone'
| 'anonymous'
| 'google.com'
| 'facebook.com'
| 'github.com'
| 'twitter.com'
| 'microsoft.com'
| 'apple.com';

export type 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: Provider;

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

// Custom claims set by the developer
claims?: object;
};

// To avoid a breaking change, we accept the 'uid' option here, but
// new users should prefer 'sub' instead.
export type TokenOptions = Partial<FirebaseIdToken> & { uid?: string };

function createUnsecuredJwt(token: TokenOptions, projectId?: string): string {
// Unsecured JWTs use "none" as the algorithm.
const header = {
alg: 'none',
kid: 'fakekid'
kid: 'fakekid',
type: 'JWT'
};
// Ensure that the auth payload has a value for 'iat'.
(auth as any).iat = (auth as any).iat || 0;
// Use `uid` field as a backup when `sub` is missing.
(auth as any).sub = (auth as any).sub || (auth as any).uid;
if (!(auth as any).sub) {
throw new Error("auth must be an object with a 'sub' or 'uid' field");

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

// Remove the uid option since it's not actually part of the token spec
delete token.uid;

const payload: FirebaseIdToken = {
// Set all required fields to decent defaults
iss: `https://securetoken.google.com/${project}`,
aud: project,
iat: iat,
exp: iat + 3600,
auth_time: iat,
sub: uid,
user_id: uid,
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(auth), /*webSafe=*/ false),
base64.encodeString(JSON.stringify(payload), /*webSafe=*/ false),
signature
].join('.');
}
Expand All @@ -71,15 +164,15 @@ export function apps(): firebase.app.App[] {
export type AppOptions = {
databaseName?: string;
projectId?: string;
auth?: object;
auth?: TokenOptions;
};
/** Construct an App authenticated with options.auth. */
export function initializeTestApp(options: AppOptions): firebase.app.App {
return initializeApp(
options.auth ? createUnsecuredJwt(options.auth) : undefined,
options.databaseName,
options.projectId
);
const jwt = options.auth
? createUnsecuredJwt(options.auth, options.projectId)
: undefined;

return initializeApp(jwt, options.databaseName, options.projectId);
}

export type AdminAppOptions = {
Expand Down
14 changes: 13 additions & 1 deletion packages/rules-unit-testing/test/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,19 @@ describe('Testing Module Tests', function () {
base64.decodeString(token!.accessToken.split('.')[1], /*webSafe=*/ false)
);
// We add an 'iat' field.
expect(claims).to.deep.equal({ uid: auth.uid, iat: 0, sub: auth.uid });
expect(claims).to.deep.equal({
iss: 'https://securetoken.google.com/foo',
aud: 'foo',
iat: 0,
exp: 3600,
auth_time: 0,
sub: 'alice',
user_id: 'alice',
firebase: {
sign_in_provider: 'custom',
identities: {}
}
});
});

it('initializeAdminApp() has admin access', async function () {
Expand Down