Skip to content

Commit 40c25e8

Browse files
nrsimrsgowman
andauthored
feat(auth): Link federatedid (#345)
* Add API to link/unlink provider info to/from user record. * Make changes in respond to first round of feedback. * review feedback * Update API to reflect changes made during api review * review feedback * review feedback Co-authored-by: Rich Gowman <[email protected]> Co-authored-by: Rich Gowman <[email protected]>
1 parent 589f08b commit 40c25e8

File tree

3 files changed

+213
-2
lines changed

3 files changed

+213
-2
lines changed

src/main/java/com/google/firebase/auth/UserRecord.java

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,29 @@ public UpdateRequest setPhoneNumber(@Nullable String phone) {
475475
if (phone != null) {
476476
checkPhoneNumber(phone);
477477
}
478+
479+
if (phone == null && properties.containsKey("deleteProvider")) {
480+
Object deleteProvider = properties.get("deleteProvider");
481+
if (deleteProvider != null) {
482+
// Due to java's type erasure, we can't fully check the type. :(
483+
@SuppressWarnings("unchecked")
484+
Iterable<String> deleteProviderIterable = (Iterable<String>)deleteProvider;
485+
486+
// If we've been told to unlink the phone provider both via setting phoneNumber to null
487+
// *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though
488+
// it might also be reasonable to relax this restriction and just unlink it.
489+
for (String dp : deleteProviderIterable) {
490+
if (dp == "phone") {
491+
throw new IllegalArgumentException(
492+
"Both UpdateRequest.setPhoneNumber(null) and "
493+
+ "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a "
494+
+ "phone provider, only specify UpdateRequest.setPhoneNumber(null).");
495+
496+
}
497+
}
498+
}
499+
}
500+
478501
properties.put("phoneNumber", phone);
479502
return this;
480503
}
@@ -548,6 +571,52 @@ public UpdateRequest setCustomClaims(Map<String,Object> customClaims) {
548571
return this;
549572
}
550573

574+
/**
575+
* Links this user to the specified provider.
576+
*
577+
* <p>Linking a provider to an existing user account does not invalidate the
578+
* refresh token of that account. In other words, the existing account
579+
* would continue to be able to access resources, despite not having used
580+
* the newly linked provider to log in. If you wish to force the user to
581+
* authenticate with this new provider, you need to (a) revoke their
582+
* refresh token (see
583+
* https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens),
584+
* and (b) ensure no other authentication methods are present on this
585+
* account.
586+
*
587+
* @param providerToLink provider info to be linked to this user\'s account.
588+
*/
589+
public UpdateRequest setProviderToLink(@NonNull UserProvider providerToLink) {
590+
properties.put("linkProviderUserInfo", checkNotNull(providerToLink));
591+
return this;
592+
}
593+
594+
/**
595+
* Unlinks this user from the specified providers.
596+
*
597+
* @param providerIds list of identifiers for the identity providers.
598+
*/
599+
public UpdateRequest setProvidersToUnlink(Iterable<String> providerIds) {
600+
checkNotNull(providerIds);
601+
for (String id : providerIds) {
602+
checkArgument(!Strings.isNullOrEmpty(id), "providerIds must not be null or empty");
603+
604+
if (id == "phone" && properties.containsKey("phoneNumber")
605+
&& properties.get("phoneNumber") == null) {
606+
// If we've been told to unlink the phone provider both via setting phoneNumber to null
607+
// *and* by setting providersToUnlink to include 'phone', then we'll reject that. Though
608+
// it might also be reasonable to relax this restriction and just unlink it.
609+
throw new IllegalArgumentException(
610+
"Both UpdateRequest.setPhoneNumber(null) and "
611+
+ "UpdateRequest.setProvidersToUnlink(['phone']) were set. To unlink from a phone "
612+
+ "provider, only specify UpdateRequest.setPhoneNumber(null).");
613+
}
614+
}
615+
616+
properties.put("deleteProvider", providerIds);
617+
return this;
618+
}
619+
551620
UpdateRequest setValidSince(long epochSeconds) {
552621
checkValidSince(epochSeconds);
553622
properties.put("validSince", epochSeconds);
@@ -569,7 +638,20 @@ Map<String, Object> getProperties(JsonFactory jsonFactory) {
569638
}
570639

571640
if (copy.containsKey("phoneNumber") && copy.get("phoneNumber") == null) {
572-
copy.put("deleteProvider", ImmutableList.of("phone"));
641+
Object deleteProvider = copy.get("deleteProvider");
642+
if (deleteProvider != null) {
643+
// Due to java's type erasure, we can't fully check the type. :(
644+
@SuppressWarnings("unchecked")
645+
Iterable<String> deleteProviderIterable = (Iterable<String>)deleteProvider;
646+
647+
copy.put("deleteProvider", new ImmutableList.Builder<String>()
648+
.addAll(deleteProviderIterable)
649+
.add("phone")
650+
.build());
651+
} else {
652+
copy.put("deleteProvider", ImmutableList.of("phone"));
653+
}
654+
573655
copy.remove("phoneNumber");
574656
}
575657

src/test/java/com/google/firebase/auth/FirebaseAuthIT.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static org.junit.Assert.assertEquals;
2020
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertNotEquals;
2122
import static org.junit.Assert.assertNotNull;
2223
import static org.junit.Assert.assertNull;
2324
import static org.junit.Assert.assertTrue;
@@ -336,6 +337,65 @@ public void testUserLifecycle() throws Exception {
336337
assertEquals(2, userRecord.getProviderData().length);
337338
assertTrue(userRecord.getCustomClaims().isEmpty());
338339

340+
// Link user to IDP providers
341+
request = userRecord.updateRequest()
342+
.setProviderToLink(
343+
UserProvider
344+
.builder()
345+
.setUid("testuid")
346+
.setProviderId("google.com")
347+
.setEmail("[email protected]")
348+
.setDisplayName("Test User")
349+
.setPhotoUrl("https://test.com/user.png")
350+
.build());
351+
userRecord = auth.updateUserAsync(request).get();
352+
assertEquals(uid, userRecord.getUid());
353+
assertEquals("Updated Name", userRecord.getDisplayName());
354+
assertEquals(randomUser.getEmail(), userRecord.getEmail());
355+
assertEquals(randomUser.getPhoneNumber(), userRecord.getPhoneNumber());
356+
assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
357+
assertTrue(userRecord.isEmailVerified());
358+
assertFalse(userRecord.isDisabled());
359+
assertEquals(3, userRecord.getProviderData().length);
360+
List<String> providers = new ArrayList<>();
361+
for (UserInfo provider : userRecord.getProviderData()) {
362+
providers.add(provider.getProviderId());
363+
}
364+
assertTrue(providers.contains("google.com"));
365+
assertTrue(userRecord.getCustomClaims().isEmpty());
366+
367+
// Unlink phone provider
368+
request = userRecord.updateRequest().setProvidersToUnlink(ImmutableList.of("phone"));
369+
userRecord = auth.updateUserAsync(request).get();
370+
assertNull(userRecord.getPhoneNumber());
371+
assertEquals(2, userRecord.getProviderData().length);
372+
providers.clear();
373+
for (UserInfo provider : userRecord.getProviderData()) {
374+
providers.add(provider.getProviderId());
375+
}
376+
assertFalse(providers.contains("phone"));
377+
assertEquals(uid, userRecord.getUid());
378+
assertEquals("Updated Name", userRecord.getDisplayName());
379+
assertEquals(randomUser.getEmail(), userRecord.getEmail());
380+
assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
381+
assertTrue(userRecord.isEmailVerified());
382+
assertFalse(userRecord.isDisabled());
383+
assertTrue(userRecord.getCustomClaims().isEmpty());
384+
385+
// Unlink IDP provider
386+
request = userRecord.updateRequest().setProvidersToUnlink(ImmutableList.of("google.com"));
387+
userRecord = auth.updateUserAsync(request).get();
388+
assertEquals(1, userRecord.getProviderData().length);
389+
assertNotEquals("google.com", userRecord.getProviderData()[0].getProviderId());
390+
assertEquals(uid, userRecord.getUid());
391+
assertEquals("Updated Name", userRecord.getDisplayName());
392+
assertEquals(randomUser.getEmail(), userRecord.getEmail());
393+
assertNull(userRecord.getPhoneNumber());
394+
assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
395+
assertTrue(userRecord.isEmailVerified());
396+
assertFalse(userRecord.isDisabled());
397+
assertTrue(userRecord.getCustomClaims().isEmpty());
398+
339399
// Get user by email
340400
userRecord = auth.getUserByEmailAsync(userRecord.getEmail()).get();
341401
assertEquals(uid, userRecord.getUid());

src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ public class FirebaseUserManagerTest {
8585

8686
private static final Map<String, Object> ACTION_CODE_SETTINGS_MAP =
8787
ACTION_CODE_SETTINGS.getProperties();
88+
private static final UserProvider USER_PROVIDER = UserProvider.builder()
89+
.setUid("testuid")
90+
.setProviderId("facebook.com")
91+
.setEmail("[email protected]")
92+
.setDisplayName("Test User")
93+
.setPhotoUrl("https://test.com/user.png")
94+
.build();
8895

8996
private static final String PROJECT_BASE_URL =
9097
"https://identitytoolkit.googleapis.com/v2/projects/test-project-id";
@@ -1147,8 +1154,10 @@ public void testUserUpdater() throws IOException {
11471154
.setEmailVerified(true)
11481155
.setPassword("secret")
11491156
.setCustomClaims(claims)
1157+
.setProviderToLink(USER_PROVIDER)
1158+
.setProvidersToUnlink(ImmutableList.of("google.com"))
11501159
.getProperties(JSON_FACTORY);
1151-
assertEquals(8, map.size());
1160+
assertEquals(10, map.size());
11521161
assertEquals(update.getUid(), map.get("localId"));
11531162
assertEquals("Display Name", map.get("displayName"));
11541163
assertEquals("http://test.com/example.png", map.get("photoUrl"));
@@ -1157,6 +1166,8 @@ public void testUserUpdater() throws IOException {
11571166
assertTrue((Boolean) map.get("emailVerified"));
11581167
assertEquals("secret", map.get("password"));
11591168
assertEquals(JSON_FACTORY.toString(claims), map.get("customAttributes"));
1169+
assertEquals(USER_PROVIDER, map.get("linkProviderUserInfo"));
1170+
assertEquals(ImmutableList.of("google.com"), map.get("deleteProvider"));
11601171
}
11611172

11621173
@Test
@@ -1194,6 +1205,64 @@ public void testEmptyCustomClaims() {
11941205
assertEquals("{}", map.get("customAttributes"));
11951206
}
11961207

1208+
@Test
1209+
public void testLinkProvider() {
1210+
UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test");
1211+
Map<String, Object> map = update
1212+
.setProviderToLink(USER_PROVIDER)
1213+
.getProperties(Utils.getDefaultJsonFactory());
1214+
assertEquals(2, map.size());
1215+
assertEquals(update.getUid(), map.get("localId"));
1216+
assertEquals(USER_PROVIDER, map.get("linkProviderUserInfo"));
1217+
}
1218+
1219+
@Test
1220+
public void testDeleteProvider() {
1221+
UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test");
1222+
Map<String, Object> map = update
1223+
.setProvidersToUnlink(ImmutableList.of("google.com"))
1224+
.getProperties(Utils.getDefaultJsonFactory());
1225+
assertEquals(2, map.size());
1226+
assertEquals(update.getUid(), map.get("localId"));
1227+
assertEquals(ImmutableList.of("google.com"), map.get("deleteProvider"));
1228+
}
1229+
1230+
@Test
1231+
public void testDeleteProviderAndPhone() {
1232+
UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("test");
1233+
Map<String, Object> map = update
1234+
.setProvidersToUnlink(ImmutableList.of("google.com"))
1235+
.setPhoneNumber(null)
1236+
.getProperties(Utils.getDefaultJsonFactory());
1237+
assertEquals(2, map.size());
1238+
assertEquals(update.getUid(), map.get("localId"));
1239+
assertEquals(ImmutableList.of("google.com", "phone"), map.get("deleteProvider"));
1240+
}
1241+
1242+
@Test
1243+
public void testDoubleDeletePhoneProvider() throws Exception {
1244+
UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("uid")
1245+
.setPhoneNumber(null);
1246+
1247+
try {
1248+
update.setProvidersToUnlink(ImmutableList.of("phone"));
1249+
fail("No error thrown for double delete phone provider");
1250+
} catch (IllegalArgumentException expected) {
1251+
}
1252+
}
1253+
1254+
@Test
1255+
public void testDoubleDeletePhoneProviderReverseOrder() throws Exception {
1256+
UserRecord.UpdateRequest update = new UserRecord.UpdateRequest("uid")
1257+
.setProvidersToUnlink(ImmutableList.of("phone"));
1258+
1259+
try {
1260+
update.setPhoneNumber(null);
1261+
fail("No error thrown for double delete phone provider");
1262+
} catch (IllegalArgumentException expected) {
1263+
}
1264+
}
1265+
11971266
@Test
11981267
public void testDeleteDisplayName() {
11991268
Map<String, Object> map = new UserRecord.UpdateRequest("test")

0 commit comments

Comments
 (0)