Skip to content

feat(auth, multi-tenant): add multi-tenant (tenantID) support #5019

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

Merged
merged 5 commits into from
Mar 12, 2021
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
1 change: 1 addition & 0 deletions .github/workflows/tests_e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ jobs:
export SKIP_BUNDLING=1
export RCT_NO_LAUNCH_PACKAGER=1
cd tests
set -o pipefail
./node_modules/.bin/detox build --configuration ios.ci
shell: bash

Expand Down
19 changes: 19 additions & 0 deletions packages/auth/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,23 @@ describe('Auth', function () {
expect(bar).toEqual(['10.0.2.2', 9099]);
});
});

describe('tenantId', function () {
it('should be able to set tenantId ', function () {
const auth = firebase.app().auth();
auth.setTenantId('test-id').then(() => {
expect(auth.tenantId).toBe('test-id');
});
});

it('should throw error when tenantId is a non string object ', async function () {
try {
await firebase.app().auth().setTenantId(Object());
return Promise.reject('It should throw an error');
} catch (e) {
expect(e.message).toBe("firebase.auth().setTenantId(*) expected 'tenantId' to be a string");
return Promise.resolve('Error catched');
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,19 @@ public void setLanguageCode(String appName, String code) {
}
}

/**
* setTenantId
*
* @param appName
* @param tenantId
*/
@ReactMethod
public void setTenantId(String appName, String tenantId) {
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
firebaseAuth.setTenantId(tenantId);
}

/**
* useDeviceLanguage
*
Expand Down Expand Up @@ -1957,6 +1970,7 @@ private WritableMap firebaseUserToMap(FirebaseUser user) {
final String provider = user.getProviderId();
final boolean verified = user.isEmailVerified();
final String phoneNumber = user.getPhoneNumber();
final String tenantId = user.getTenantId();

userMap.putString("uid", uid);
userMap.putString("providerId", provider);
Expand Down Expand Up @@ -1987,6 +2001,12 @@ private WritableMap firebaseUserToMap(FirebaseUser user) {
userMap.putNull("phoneNumber");
}

if (tenantId != null && !"".equals(tenantId)) {
userMap.putString("tenantId", tenantId);
} else {
userMap.putNull("tenantId");
}

userMap.putArray("providerData", convertProviderData(user.getProviderData(), user));

WritableMap metadataMap = Arguments.createMap();
Expand Down
21 changes: 21 additions & 0 deletions packages/auth/e2e/auth.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -1071,4 +1071,25 @@ describe('auth()', function () {
}
});
});

describe('setTenantId()', function () {
it('should return null if tenantId unset', function () {
should.not.exist(firebase.auth().tenantId);
});

// multi-tenant is not supported by the firebase auth emulator, and requires a valid multi-tenant tenantid
// After setting this, next user creation will result in internal error on emulator, or auth/invalid-tenant-id live
// it('should return tenantId correctly after setting', async function () {
// await firebase.auth().setTenantId('testTenantId');
// firebase.auth().tenantId.should.equal('testTenantId');
// });
// it('user should have tenant after setting tenantId', async function () {
// await firebase.auth().setTenantId('userTestTenantId');
// firebase.auth().tenantId.should.equal('userTestTenantId');
// const random = Utils.randString(12, '#a');
// const email = `${random}@${random}.com`;
// const userCredential = await firebase.auth().createUserWithEmailAndPassword(email, random);
// userCredential.user.tenantId.should.equal('userTestTenantId');
// });
});
});
8 changes: 8 additions & 0 deletions packages/auth/ios/RNFBAuth/RNFBAuthModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,13 @@ - (void)invalidate {

}

RCT_EXPORT_METHOD(setTenantId:
(FIRApp *) firebaseApp
:(NSString *) tenantID
) {
[FIRAuth authWithApp:firebaseApp].tenantID = tenantID;
}

RCT_EXPORT_METHOD(useDeviceLanguage:
(FIRApp *) firebaseApp
) {
Expand Down Expand Up @@ -1203,6 +1210,7 @@ - (NSDictionary *)firebaseUserToDict:(FIRUser *)user {
@"providerData": [self convertProviderData:user.providerData],
keyProviderId: [user.providerID lowercaseString],
@"refreshToken": user.refreshToken,
@"tenantId": user.tenantID ? (id) user.tenantID : [NSNull null],
keyUid: user.uid
};
}
Expand Down
4 changes: 4 additions & 0 deletions packages/auth/lib/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export default class User {
return this._user.phoneNumber || null;
}

get tenantId() {
return this._user.tenantId || null;
}

get photoURL() {
return this._user.photoURL || null;
}
Expand Down
27 changes: 27 additions & 0 deletions packages/auth/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ export namespace FirebaseAuthTypes {
* Returns the unique identifier of the provider type that this instance corresponds to.
*/
providerId: string;
/**
* Returns a string representing the multi-tenant tenant id. This is null if the user is not associated with a tenant.
*/
tenantId?: string;
/**
* Returns a user identifier as specified by the authentication provider.
*/
Expand Down Expand Up @@ -1201,6 +1205,16 @@ export namespace FirebaseAuthTypes {
* TODO @salakar missing updateCurrentUser
*/
export class Module extends FirebaseModule {
/**
* Returns the current tenant Id or null if it has never been set
*
* #### Example
*
* ```js
* const tenantId = firebase.auth().tenantId;
* ```
*/
tenantId: string | null;
/**
* Returns the current language code.
*
Expand Down Expand Up @@ -1228,6 +1242,19 @@ export namespace FirebaseAuthTypes {
* > It is recommended to use {@link auth#onAuthStateChanged} to track whether the user is currently signed in.
*/
currentUser: User | null;
/**
* Sets the tenant id.
*
* #### Example
*
* ```js
* await firebase.auth().setTenantId('tenant-123');
* ```
*
* @error auth/invalid-tenant-id if the tenant id is invalid for some reason
* @param tenantId the tenantID current app bind to.
*/
setTenantId(tenantId: string): Promise<void>;
/**
* Sets the language code.
*
Expand Down
13 changes: 13 additions & 0 deletions packages/auth/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class FirebaseAuthModule extends FirebaseModule {
this._settings = null;
this._authResult = false;
this._languageCode = this.native.APP_LANGUAGE[this.app._name];
this._tenantId = null;

if (!this.languageCode) {
this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]'];
Expand Down Expand Up @@ -101,6 +102,10 @@ class FirebaseAuthModule extends FirebaseModule {
return this._languageCode;
}

get tenantId() {
return this._tenantId;
}

get settings() {
if (!this._settings) {
this._settings = new Settings(this);
Expand Down Expand Up @@ -150,6 +155,14 @@ class FirebaseAuthModule extends FirebaseModule {
}
}

async setTenantId(tenantId) {
if (!isString(tenantId)) {
throw new Error("firebase.auth().setTenantId(*) expected 'tenantId' to be a string");
}
this._tenantId = tenantId;
await this.native.setTenantId(tenantId);
}

_parseListener(listenerOrObserver) {
return typeof listenerOrObserver === 'object'
? listenerOrObserver.next.bind(listenerOrObserver)
Expand Down
6 changes: 3 additions & 3 deletions tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@
"configurations": {
"ios.sim.debug": {
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/testing.app",
"build": "xcodebuild -workspace ios/testing.xcworkspace -scheme testing -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -quiet | xcpretty -k",
"build": "set -o pipefail && xcodebuild -workspace ios/testing.xcworkspace -scheme testing -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -quiet | xcpretty -k",
"type": "ios.simulator",
"device": {
"type": "iPhone 8"
}
},
"ios.ci": {
"binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/testing.app",
"build": "xcodebuild -workspace ios/testing.xcworkspace -scheme testing -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES | xcpretty -k",
"build": "set -o pipefail && xcodebuild -workspace ios/testing.xcworkspace -scheme testing -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES | xcpretty -k",
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
Expand All @@ -76,7 +76,7 @@
},
"ios.sim.release": {
"binaryPath": "ios/build/Build/Products/Release-iphonesimulator/testing.app",
"build": "export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/testing.xcworkspace -scheme testing -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -quiet | xcpretty -k",
"build": "set -o pipefail && export RCT_NO_LAUNCH_PACKAGER=true && xcodebuild -workspace ios/testing.xcworkspace -scheme testing -configuration Release -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -quiet | xcpretty -k",
"type": "ios.simulator",
"device": {
"type": "iPhone X"
Expand Down