Skip to content

Commit f87eb8b

Browse files
authored
Merge 39cd8ff into a10c18f
2 parents a10c18f + 39cd8ff commit f87eb8b

File tree

3 files changed

+127
-17
lines changed

3 files changed

+127
-17
lines changed

.changeset/chilled-ways-promise.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/rules-unit-testing': patch
3+
---
4+
5+
Add stronger types to the 'options.auth' option for initializeTestApp

packages/rules-unit-testing/src/api/index.ts

+109-16
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,118 @@ let _databaseHost: string | undefined = undefined;
4141
/** The actual address for the Firestore emulator */
4242
let _firestoreHost: string | undefined = undefined;
4343

44-
/** Create an unsecured JWT for the given auth payload. See https://tools.ietf.org/html/rfc7519#section-6. */
45-
function createUnsecuredJwt(auth: object): string {
44+
export type Provider =
45+
| 'custom'
46+
| 'email'
47+
| 'password'
48+
| 'phone'
49+
| 'anonymous'
50+
| 'google.com'
51+
| 'facebook.com'
52+
| 'github.com'
53+
| 'twitter.com'
54+
| 'microsoft.com'
55+
| 'apple.com';
56+
57+
export type FirebaseIdToken = {
58+
// Always set to https://securetoken.google.com/PROJECT_ID
59+
iss: string;
60+
61+
// Always set to PROJECT_ID
62+
aud: string;
63+
64+
// The user's unique id
65+
sub: string;
66+
67+
// The token issue time, in seconds since epoch
68+
iat: number;
69+
70+
// The token expiry time, normally 'iat' + 3600
71+
exp: number;
72+
73+
// The user's unique id, must be equal to 'sub'
74+
user_id: string;
75+
76+
// The time the user authenticated, normally 'iat'
77+
auth_time: number;
78+
79+
// The sign in provider, only set when the provider is 'anonymous'
80+
provider_id?: 'anonymous';
81+
82+
// The user's primary email
83+
email?: string;
84+
85+
// The user's email verification status
86+
email_verified?: boolean;
87+
88+
// The user's primary phone number
89+
phone_number?: string;
90+
91+
// The user's display name
92+
name?: string;
93+
94+
// The user's profile photo URL
95+
picture?: string;
96+
97+
// Information on all identities linked to this user
98+
firebase: {
99+
// The primary sign-in provider
100+
sign_in_provider: Provider;
101+
102+
// A map of providers to the user's list of unique identifiers from
103+
// each provider
104+
identities?: { [provider in Provider]?: string[] };
105+
};
106+
107+
// Custom claims set by the developer
108+
claims?: object;
109+
};
110+
111+
// To avoid a breaking change, we accept the 'uid' option here, but
112+
// new users should prefer 'sub' instead.
113+
export type TokenOptions = Partial<FirebaseIdToken> & { uid?: string };
114+
115+
function createUnsecuredJwt(token: TokenOptions, projectId?: string): string {
46116
// Unsecured JWTs use "none" as the algorithm.
47117
const header = {
48118
alg: 'none',
49-
kid: 'fakekid'
119+
kid: 'fakekid',
120+
type: 'JWT'
50121
};
51-
// Ensure that the auth payload has a value for 'iat'.
52-
(auth as any).iat = (auth as any).iat || 0;
53-
// Use `uid` field as a backup when `sub` is missing.
54-
(auth as any).sub = (auth as any).sub || (auth as any).uid;
55-
if (!(auth as any).sub) {
56-
throw new Error("auth must be an object with a 'sub' or 'uid' field");
122+
123+
const project = projectId || 'fake-project';
124+
const iat = token.iat || 0;
125+
const uid = token.sub || token.uid || token.user_id;
126+
if (!uid) {
127+
throw new Error("Auth must contain 'sub', 'uid', or 'user_id' field!");
57128
}
129+
130+
// Remove the uid option since it's not actually part of the token spec
131+
delete token.uid;
132+
133+
const payload: FirebaseIdToken = {
134+
// Set all required fields to decent defaults
135+
iss: `https://securetoken.google.com/${project}`,
136+
aud: project,
137+
iat: iat,
138+
exp: iat + 3600,
139+
auth_time: iat,
140+
sub: uid,
141+
user_id: uid,
142+
firebase: {
143+
sign_in_provider: 'custom',
144+
identities: {}
145+
},
146+
147+
// Override with user options
148+
...token
149+
};
150+
58151
// Unsecured JWTs use the empty string as a signature.
59152
const signature = '';
60153
return [
61154
base64.encodeString(JSON.stringify(header), /*webSafe=*/ false),
62-
base64.encodeString(JSON.stringify(auth), /*webSafe=*/ false),
155+
base64.encodeString(JSON.stringify(payload), /*webSafe=*/ false),
63156
signature
64157
].join('.');
65158
}
@@ -71,15 +164,15 @@ export function apps(): firebase.app.App[] {
71164
export type AppOptions = {
72165
databaseName?: string;
73166
projectId?: string;
74-
auth?: object;
167+
auth?: TokenOptions;
75168
};
76169
/** Construct an App authenticated with options.auth. */
77170
export function initializeTestApp(options: AppOptions): firebase.app.App {
78-
return initializeApp(
79-
options.auth ? createUnsecuredJwt(options.auth) : undefined,
80-
options.databaseName,
81-
options.projectId
82-
);
171+
const jwt = options.auth
172+
? createUnsecuredJwt(options.auth, options.projectId)
173+
: undefined;
174+
175+
return initializeApp(jwt, options.databaseName, options.projectId);
83176
}
84177

85178
export type AdminAppOptions = {

packages/rules-unit-testing/test/database.test.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,19 @@ describe('Testing Module Tests', function () {
121121
base64.decodeString(token!.accessToken.split('.')[1], /*webSafe=*/ false)
122122
);
123123
// We add an 'iat' field.
124-
expect(claims).to.deep.equal({ uid: auth.uid, iat: 0, sub: auth.uid });
124+
expect(claims).to.deep.equal({
125+
iss: 'https://securetoken.google.com/foo',
126+
aud: 'foo',
127+
iat: 0,
128+
exp: 3600,
129+
auth_time: 0,
130+
sub: 'alice',
131+
user_id: 'alice',
132+
firebase: {
133+
sign_in_provider: 'custom',
134+
identities: {}
135+
}
136+
});
125137
});
126138

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

0 commit comments

Comments
 (0)