Skip to content

Commit b4d1547

Browse files
authored
Merge d44fc4c into 53a9263
2 parents 53a9263 + d44fc4c commit b4d1547

File tree

14 files changed

+465
-10
lines changed

14 files changed

+465
-10
lines changed

packages/auth-compat/test/helpers/helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
getEmulatorUrl
2626
} from '../../../auth/test/helpers/integration/settings';
2727
import { resetEmulator } from '../../../auth/test/helpers/integration/emulator_rest_helpers';
28+
export { createNewTenant } from '../../../auth/test/helpers/integration/emulator_rest_helpers';
2829

2930
export function initializeTestInstance(): void {
3031
firebase.initializeApp(getAppConfig());
@@ -34,6 +35,7 @@ export function initializeTestInstance(): void {
3435
}
3536

3637
export async function cleanUpTestInstance(): Promise<void> {
38+
await firebase.auth().signOut();
3739
for (const app of firebase.apps) {
3840
await app.delete();
3941
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @license
3+
* Copyright 2021 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import firebase from '@firebase/app-compat';
19+
import { expect, use } from 'chai';
20+
import * as chaiAsPromised from 'chai-as-promised';
21+
import {
22+
createNewTenant,
23+
initializeTestInstance,
24+
cleanUpTestInstance
25+
} from '../../helpers/helpers';
26+
27+
use(chaiAsPromised);
28+
29+
describe('Integration test: multi-tenant', () => {
30+
let tenantA: string;
31+
let tenantB: string;
32+
33+
beforeEach(async () => {
34+
initializeTestInstance();
35+
tenantA = await createNewTenant();
36+
tenantB = await createNewTenant();
37+
});
38+
39+
afterEach(async () => {
40+
await cleanUpTestInstance();
41+
});
42+
43+
it('sets the correct tenantId on the underlying user', async () => {
44+
firebase.auth().tenantId = tenantA;
45+
const { user } = await firebase.auth().signInAnonymously();
46+
expect(user!.tenantId).to.eq(tenantA);
47+
});
48+
49+
it('allows updateCurrentUser to be called when TID matches', async () => {
50+
firebase.auth().tenantId = tenantA;
51+
const { user } = await firebase.auth().signInAnonymously();
52+
await expect(firebase.auth().updateCurrentUser(user)).not.to.be.rejected;
53+
});
54+
55+
it('throws for mismatched TID', async () => {
56+
firebase.auth().tenantId = tenantA;
57+
const { user } = await firebase.auth().signInAnonymously();
58+
firebase.auth().tenantId = tenantB;
59+
await expect(firebase.auth().updateCurrentUser(user)).to.be.rejectedWith(
60+
'auth/tenant-id-mismatch'
61+
);
62+
});
63+
64+
it('allows users to be deleted', async () => {
65+
firebase.auth().tenantId = tenantA;
66+
const { user } = await firebase.auth().signInAnonymously();
67+
await user!.delete();
68+
expect(firebase.auth().currentUser).to.be.null;
69+
});
70+
71+
// The rest of the tenantId tests are in the respective flow tests
72+
});

packages/auth-compat/test/integration/webdriver/static/core.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export function legacyAuthInit() {
3434
});
3535
}
3636

37+
export async function setTenantId(tid) {
38+
compat.auth().tenantId = tid;
39+
}
40+
3741
export async function userSnap() {
3842
return compat.auth().currentUser;
3943
}

packages/auth/test/helpers/integration/emulator_rest_helpers.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import * as fetchImpl from 'node-fetch';
19-
import { getAppConfig, getEmulatorUrl } from './settings';
19+
import { API_KEY, getAppConfig, getEmulatorUrl } from './settings';
2020

2121
export interface VerificationSession {
2222
code: string;
@@ -51,8 +51,8 @@ export async function getPhoneVerificationCodes(): Promise<
5151
}, {} as Record<string, VerificationSession>);
5252
}
5353

54-
export async function getOobCodes(): Promise<OobCodeSession[]> {
55-
const url = buildEmulatorUrlForPath('oobCodes');
54+
export async function getOobCodes(tenantId?: string): Promise<OobCodeSession[]> {
55+
const url = buildEmulatorUrlForPath('oobCodes', tenantId);
5656
const response: OobCodesResponse = await (await doFetch(url)).json();
5757
return response.oobCodes;
5858
}
@@ -78,10 +78,31 @@ export async function createAnonAccount(): Promise<{
7878
return response;
7979
}
8080

81-
function buildEmulatorUrlForPath(endpoint: string): string {
81+
export async function createNewTenant(): Promise<string> {
82+
const projectId = getAppConfig().projectId;
83+
const url = `${getEmulatorUrl()}/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants?key=${API_KEY}`;
84+
const request = {
85+
name: 'tenant',
86+
allowPasswordSignup: true,
87+
enableEmailLinkSignin: true,
88+
disableAuth: false,
89+
enableAnonymousUser: true,
90+
};
91+
const response = await (
92+
await doFetch(url, {
93+
method: 'POST',
94+
body: JSON.stringify(request),
95+
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer owner'}
96+
})
97+
).json();
98+
return response.tenantId;
99+
}
100+
101+
function buildEmulatorUrlForPath(endpoint: string, tenantId?: string): string {
82102
const emulatorBaseUrl = getEmulatorUrl();
83103
const projectId = getAppConfig().projectId;
84-
return `${emulatorBaseUrl}/emulator/v1/projects/${projectId}/${endpoint}`;
104+
const tenantScope = tenantId ? `/tenants/${tenantId}` : '';
105+
return `${emulatorBaseUrl}/emulator/v1/projects/${projectId}${tenantScope}/${endpoint}`;
85106
}
86107

87108
function doFetch(url: string, request?: RequestInit): ReturnType<typeof fetch> {

packages/auth/test/integration/flows/anonymous.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ describe('Integration test: anonymous auth', () => {
5555
expect(user.uid).to.be.a('string');
5656
});
5757

58+
// Multi-tenancy for anon auth covered by multi_tenant.local.test.ts
59+
5860
it('second sign in on the same device yields same user', async () => {
5961
const { user: userA } = await signInAnonymously(auth);
6062
const { user: userB } = await signInAnonymously(auth);

packages/auth/test/integration/flows/custom.local.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
import { FirebaseError } from '@firebase/util';
3535
import { expect, use } from 'chai';
3636
import * as chaiAsPromised from 'chai-as-promised';
37+
import { createNewTenant } from '../../helpers/integration/emulator_rest_helpers';
3738
import {
3839
cleanUpTestInstance,
3940
getTestInstance,
@@ -79,6 +80,26 @@ describe('Integration test: custom auth', () => {
7980
expect(additionalUserInfo.isNewUser).to.be.true;
8081
});
8182

83+
it('signs in with custom token in tenant project', async () => {
84+
const tenantId = await createNewTenant();
85+
auth.tenantId = tenantId;
86+
const cred = await signInWithCustomToken(auth, customToken);
87+
expect(auth.currentUser).to.eq(cred.user);
88+
expect(cred.operationType).to.eq(OperationType.SIGN_IN);
89+
90+
const { user } = cred;
91+
expect(user.isAnonymous).to.be.false;
92+
expect(user.uid).to.eq(uid);
93+
expect(user.tenantId).to.eq(tenantId);
94+
expect((await user.getIdTokenResult(false)).claims.customClaim).to.eq(
95+
'some-claim'
96+
);
97+
expect(user.providerId).to.eq('firebase');
98+
const additionalUserInfo = await getAdditionalUserInfo(cred)!;
99+
expect(additionalUserInfo.providerId).to.be.null;
100+
expect(additionalUserInfo.isNewUser).to.be.true;
101+
});
102+
82103
it('uid will overwrite existing user, joining accounts', async () => {
83104
const { user: anonUser } = await signInAnonymously(auth);
84105
const customCred = await signInWithCustomToken(

packages/auth/test/integration/flows/email.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
getTestInstance,
3939
randomEmail
4040
} from '../../helpers/integration/helpers';
41+
import { createNewTenant } from '../../helpers/integration/emulator_rest_helpers';
4142

4243
use(chaiAsPromised);
4344

@@ -74,6 +75,32 @@ describe('Integration test: email/password auth', () => {
7475
expect(additionalUserInfo.providerId).to.eq('password');
7576
});
7677

78+
it('allows user to sign up in tenant project', async () => {
79+
const tenantId = await createNewTenant();
80+
auth.tenantId = tenantId;
81+
const userCred = await createUserWithEmailAndPassword(
82+
auth,
83+
email,
84+
'password'
85+
);
86+
expect(auth.currentUser).to.eq(userCred.user);
87+
expect(userCred.operationType).to.eq(OperationType.SIGN_IN);
88+
89+
const user = userCred.user;
90+
expect(user.isAnonymous).to.be.false;
91+
expect(user.uid).to.be.a('string');
92+
expect(user.email).to.eq(email);
93+
expect(user.emailVerified).to.be.false;
94+
expect(user.providerData.length).to.eq(1);
95+
expect(user.providerData[0].providerId).to.eq('password');
96+
expect(user.providerData[0].email).to.eq(email);
97+
expect(user.tenantId).to.eq(tenantId);
98+
99+
const additionalUserInfo = getAdditionalUserInfo(userCred)!;
100+
expect(additionalUserInfo.isNewUser).to.be.true;
101+
expect(additionalUserInfo.providerId).to.eq('password');
102+
});
103+
77104
it('errors when createUser called twice', async () => {
78105
await createUserWithEmailAndPassword(auth, email, 'password');
79106
await expect(

packages/auth/test/integration/flows/idp.local.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { FirebaseError } from '@firebase/util';
3737
import { expect, use } from 'chai';
3838
import * as chaiAsPromised from 'chai-as-promised';
39+
import { createNewTenant } from '../../helpers/integration/emulator_rest_helpers';
3940
import {
4041
cleanUpTestInstance,
4142
getTestInstance,
@@ -89,6 +90,31 @@ describe('Integration test: headless IdP', () => {
8990
expect(additionalUserInfo.providerId).to.eq('google.com');
9091
});
9192

93+
it('signs in with an OAuth token in a tenant', async () => {
94+
const tenantId = await createNewTenant();
95+
auth.tenantId = tenantId;
96+
const cred = await signInWithCredential(
97+
auth,
98+
GoogleAuthProvider.credential(oauthIdToken)
99+
);
100+
expect(auth.currentUser).to.eq(cred.user);
101+
expect(cred.operationType).to.eq(OperationType.SIGN_IN);
102+
103+
// Make sure the user is setup correctly
104+
const { user } = cred;
105+
expect(user.isAnonymous).to.be.false;
106+
expect(user.emailVerified).to.be.true;
107+
expect(user.providerData.length).to.eq(1);
108+
expect(user.providerData[0].providerId).to.eq('google.com');
109+
expect(user.providerData[0].email).to.eq(email);
110+
expect(user.tenantId).to.eq(tenantId);
111+
112+
// Make sure the additional user info is good
113+
const additionalUserInfo = getAdditionalUserInfo(cred)!;
114+
expect(additionalUserInfo.isNewUser).to.be.true;
115+
expect(additionalUserInfo.providerId).to.eq('google.com');
116+
});
117+
92118
it('allows the user to update profile', async () => {
93119
const credential = GithubAuthProvider.credential(oauthIdToken);
94120
const { user } = await signInWithCredential(auth, credential);
@@ -110,6 +136,30 @@ describe('Integration test: headless IdP', () => {
110136
expect(auth.currentUser!.photoURL).to.eq('http://photo.test/david.png');
111137
});
112138

139+
it('allows the user to update profile in a tenant', async () => {
140+
const tenantId = await createNewTenant();
141+
auth.tenantId = tenantId;
142+
const credential = GithubAuthProvider.credential(oauthIdToken);
143+
const { user } = await signInWithCredential(auth, credential);
144+
145+
await updateProfile(user, {
146+
displayName: 'David Copperfield',
147+
photoURL: 'http://photo.test/david.png'
148+
});
149+
150+
// Check everything first
151+
expect(user.displayName).to.eq('David Copperfield');
152+
expect(user.photoURL).to.eq('http://photo.test/david.png');
153+
154+
await auth.signOut();
155+
156+
// Sign in again and double check; look at current user this time
157+
await signInWithCredential(auth, credential);
158+
expect(auth.currentUser!.displayName).to.eq('David Copperfield');
159+
expect(auth.currentUser!.photoURL).to.eq('http://photo.test/david.png');
160+
expect(auth.currentUser!.tenantId).to.eq(tenantId);
161+
});
162+
113163
it('allows the user to change the email', async () => {
114164
const credential = FacebookAuthProvider.credential(oauthIdToken);
115165
const { user } = await signInWithCredential(auth, credential);
@@ -206,6 +256,51 @@ describe('Integration test: headless IdP', () => {
206256
expect(user.providerData[0].providerId).to.eq('facebook.com');
207257
});
208258

259+
it('can link with multiple idps within a tenant', async () => {
260+
const tenantId = await createNewTenant();
261+
auth.tenantId = tenantId;
262+
const googleEmail = randomEmail();
263+
const facebookEmail = randomEmail();
264+
265+
const googleCredential = GoogleAuthProvider.credential(
266+
JSON.stringify({
267+
sub: googleEmail,
268+
email: googleEmail,
269+
'email_verified': true
270+
})
271+
);
272+
273+
const facebookCredential = FacebookAuthProvider.credential(
274+
JSON.stringify({
275+
sub: facebookEmail,
276+
email: facebookEmail
277+
})
278+
);
279+
280+
// Link and then test everything
281+
const { user } = await signInWithCredential(auth, facebookCredential);
282+
await linkWithCredential(user, googleCredential);
283+
expect(user.email).to.eq(facebookEmail);
284+
expect(user.emailVerified).to.be.false;
285+
expect(user.providerData.length).to.eq(2);
286+
expect(
287+
user.providerData.find(p => p.providerId === 'google.com')!.email
288+
).to.eq(googleEmail);
289+
expect(
290+
user.providerData.find(p => p.providerId === 'facebook.com')!.email
291+
).to.eq(facebookEmail);
292+
expect(user.tenantId).to.eq(tenantId);
293+
294+
// Unlink Google and check everything again
295+
await unlink(user, ProviderId.GOOGLE);
296+
expect(user.email).to.eq(facebookEmail);
297+
expect(user.emailVerified).to.be.false;
298+
expect(user.providerData.length).to.eq(1);
299+
expect(user.providerData[0].email).to.eq(facebookEmail);
300+
expect(user.providerData[0].providerId).to.eq('facebook.com');
301+
expect(user.tenantId).to.eq(tenantId);
302+
});
303+
209304
it('IdP account takes over unverified email', async () => {
210305
const credential = GoogleAuthProvider.credential(oauthIdToken);
211306
const { user: emailUser } = await createUserWithEmailAndPassword(

0 commit comments

Comments
 (0)