diff --git a/.changeset/chilled-ways-promise.md b/.changeset/chilled-ways-promise.md new file mode 100644 index 00000000000..80b7ebc183d --- /dev/null +++ b/.changeset/chilled-ways-promise.md @@ -0,0 +1,5 @@ +--- +'@firebase/rules-unit-testing': patch +--- + +Add stronger types to the 'options.auth' option for initializeTestApp diff --git a/packages/rules-unit-testing/src/api/index.ts b/packages/rules-unit-testing/src/api/index.ts index a2368e0fbe1..e018e815968 100644 --- a/packages/rules-unit-testing/src/api/index.ts +++ b/packages/rules-unit-testing/src/api/index.ts @@ -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 & { 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('.'); } @@ -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 = { diff --git a/packages/rules-unit-testing/test/database.test.ts b/packages/rules-unit-testing/test/database.test.ts index 6bac58310b5..0351e020619 100644 --- a/packages/rules-unit-testing/test/database.test.ts +++ b/packages/rules-unit-testing/test/database.test.ts @@ -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 () {