Skip to content

Commit bb00dec

Browse files
authored
fix: make canLinkAccountsHelper check if the login methods of the primary user have conflicts on the tenant of the link target (#1015)
* fix: make canLinkAccountsHelper check if the login methods of the primary user have conflicts on the tenant of the link target * chore: bump version number
1 parent 6c0ec50 commit bb00dec

File tree

4 files changed

+92
-14
lines changed

4 files changed

+92
-14
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
99

10+
## [9.1.1] -2024-07-24
11+
12+
### Fixes
13+
14+
- Account linking now properly checks if the login methods of the primary user can be shared with the tenants of the
15+
recipe user we are trying to link
16+
1017
## [9.1.0] - 2024-05-24
1118

1219
### Changes

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
1919
// }
2020
//}
2121

22-
version = "9.1.0"
22+
version = "9.1.1"
2323

2424

2525
repositories {

src/main/java/io/supertokens/authRecipe/AuthRecipe.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,25 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection
234234
// now we know that the recipe user ID is not a primary user, so we can focus on it's one
235235
// login method
236236
assert (recipeUser.loginMethods.length == 1);
237-
LoginMethod recipeUserIdLM = recipeUser.loginMethods[0];
238-
237+
239238
Set<String> tenantIds = new HashSet<>();
240239
tenantIds.addAll(recipeUser.tenantIds);
241240
tenantIds.addAll(primaryUser.tenantIds);
242241

242+
checkIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds, recipeUser.loginMethods[0], primaryUser);
243+
244+
for (LoginMethod currLoginMethod : primaryUser.loginMethods) {
245+
checkIfLoginMethodCanBeLinkedOnTenant(con, appIdentifier, authRecipeStorage, tenantIds, currLoginMethod, primaryUser);
246+
}
247+
248+
return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), false);
249+
}
250+
251+
private static void checkIfLoginMethodCanBeLinkedOnTenant(TransactionConnection con, AppIdentifier appIdentifier,
252+
AuthRecipeSQLStorage authRecipeStorage,
253+
Set<String> tenantIds, LoginMethod currLoginMethod,
254+
AuthRecipeUserInfo primaryUser)
255+
throws StorageQueryException, AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException {
243256
// we loop through the union of both the user's tenantIds and check that the criteria for
244257
// linking accounts is not violated in any of them. We do a union and not an intersection
245258
// cause if we did an intersection, and that yields that account linking is allowed, it could
@@ -251,17 +264,14 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection
251264
// intersection, we will get an empty set, but if we do a union, we will get both the tenants and
252265
// do the checks in both.
253266
for (String tenantId : tenantIds) {
254-
TenantIdentifier tenantIdentifier = new TenantIdentifier(
255-
appIdentifier.getConnectionUriDomain(), appIdentifier.getAppId(),
256-
tenantId);
257267
// we do not bother with getting the storage for each tenant here because
258268
// we get the tenants from the user itself, and the user can only be shared across
259269
// tenants of the same storage - therefore, the storage will be the same.
260270

261-
if (recipeUserIdLM.email != null) {
271+
if (currLoginMethod.email != null) {
262272
AuthRecipeUserInfo[] usersWithSameEmail = authRecipeStorage
263273
.listPrimaryUsersByEmail_Transaction(appIdentifier, con,
264-
recipeUserIdLM.email);
274+
currLoginMethod.email);
265275
for (AuthRecipeUserInfo user : usersWithSameEmail) {
266276
if (!user.tenantIds.contains(tenantId)) {
267277
continue;
@@ -274,10 +284,10 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection
274284
}
275285
}
276286

277-
if (recipeUserIdLM.phoneNumber != null) {
287+
if (currLoginMethod.phoneNumber != null) {
278288
AuthRecipeUserInfo[] usersWithSamePhoneNumber = authRecipeStorage
279289
.listPrimaryUsersByPhoneNumber_Transaction(appIdentifier, con,
280-
recipeUserIdLM.phoneNumber);
290+
currLoginMethod.phoneNumber);
281291
for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) {
282292
if (!user.tenantIds.contains(tenantId)) {
283293
continue;
@@ -291,10 +301,10 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection
291301
}
292302
}
293303

294-
if (recipeUserIdLM.thirdParty != null) {
304+
if (currLoginMethod.thirdParty != null) {
295305
AuthRecipeUserInfo[] usersWithSameThirdParty = authRecipeStorage
296306
.listPrimaryUsersByThirdPartyInfo_Transaction(appIdentifier, con,
297-
recipeUserIdLM.thirdParty.id, recipeUserIdLM.thirdParty.userId);
307+
currLoginMethod.thirdParty.id, currLoginMethod.thirdParty.userId);
298308
for (AuthRecipeUserInfo userWithSameThirdParty : usersWithSameThirdParty) {
299309
if (!userWithSameThirdParty.tenantIds.contains(tenantId)) {
300310
continue;
@@ -310,8 +320,6 @@ private static CanLinkAccountsResult canLinkAccountsHelper(TransactionConnection
310320

311321
}
312322
}
313-
314-
return new CanLinkAccountsResult(recipeUser.getSupertokensUserId(), primaryUser.getSupertokensUserId(), false);
315323
}
316324

317325
@TestOnly

src/test/java/io/supertokens/test/accountlinking/LinkAccountsTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,69 @@ public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUserEvenIfIn
517517
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
518518
}
519519

520+
@Test
521+
public void linkAccountFailureCauseAccountInfoAssociatedWithAPrimaryUserEvenIfInDifferentTenant2() throws Exception {
522+
String[] args = {"../"};
523+
TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false);
524+
FeatureFlagTestContent.getInstance(process.getProcess())
525+
.setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{
526+
EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY});
527+
process.startProcess();
528+
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));
529+
530+
if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
531+
return;
532+
}
533+
534+
Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null),
535+
new TenantConfig(new TenantIdentifier(null, null, "t1"), new EmailPasswordConfig(true),
536+
new ThirdPartyConfig(true, null), new PasswordlessConfig(true),
537+
null, null, new JsonObject()));
538+
539+
Multitenancy.addNewOrUpdateAppOrTenant(process.main, new TenantIdentifier(null, null, null),
540+
new TenantConfig(new TenantIdentifier(null, null, "t2"), new EmailPasswordConfig(true),
541+
new ThirdPartyConfig(true, null), new PasswordlessConfig(true),
542+
null, null, new JsonObject()));
543+
544+
Storage storage = (StorageLayer.getStorage(process.main));
545+
546+
AuthRecipeUserInfo conflictingUser =
547+
EmailPassword.signUp(new TenantIdentifier(null, null, "t2"), storage,
548+
process.getProcess(),
549+
"[email protected]", "password");
550+
assert (!conflictingUser.isPrimaryUser);
551+
AuthRecipe.createPrimaryUser(process.main, conflictingUser.getSupertokensUserId());
552+
553+
Thread.sleep(50);
554+
555+
AuthRecipeUserInfo user1 =
556+
EmailPassword.signUp(new TenantIdentifier(null, null, "t1"), storage,
557+
process.getProcess(),
558+
"[email protected]", "password");
559+
assert (!user1.isPrimaryUser);
560+
561+
AuthRecipeUserInfo user2 =
562+
EmailPassword.signUp(new TenantIdentifier(null, null, "t2"), storage,
563+
process.getProcess(),
564+
"[email protected]", "password");
565+
assert (!user1.isPrimaryUser);
566+
567+
568+
AuthRecipe.createPrimaryUser(process.main, user1.getSupertokensUserId());
569+
570+
try {
571+
AuthRecipe.linkAccounts(process.main, user2.getSupertokensUserId(),
572+
user1.getSupertokensUserId());
573+
assert (false);
574+
} catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) {
575+
assert (e.primaryUserId.equals(conflictingUser.getSupertokensUserId()));
576+
assert (e.getMessage().equals("This user's email is already associated with another user ID"));
577+
}
578+
579+
process.kill();
580+
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
581+
}
582+
520583
@Test
521584
public void linkAccountSuccessAcrossTenants() throws Exception {
522585
String[] args = {"../"};

0 commit comments

Comments
 (0)