Skip to content

Commit 9898201

Browse files
nrsimMichael Sundqvist
authored and
Michael Sundqvist
committed
Add the ability to link/unlink a provider user id to an account.
1 parent e4f5a55 commit 9898201

File tree

5 files changed

+341
-20
lines changed

5 files changed

+341
-20
lines changed

Diff for: FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/FirebaseUserManagerTest.cs

+177-9
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,40 @@ namespace FirebaseAdmin.Auth.Users.Tests
3434
{
3535
public class FirebaseUserManagerTest
3636
{
37-
public static readonly IEnumerable<object[]> TestConfigs = new List<object[]>()
37+
public static readonly TheoryData<TestConfig> TestConfigs = new TheoryData<TestConfig>()
3838
{
39-
new object[] { TestConfig.ForFirebaseAuth() },
40-
new object[] { TestConfig.ForTenantAwareFirebaseAuth("tenant1") },
41-
new object[] { TestConfig.ForFirebaseAuth().WithEmulator() },
42-
new object[] { TestConfig.ForTenantAwareFirebaseAuth("tenant1").WithEmulator() },
39+
TestConfig.ForFirebaseAuth(),
40+
TestConfig.ForTenantAwareFirebaseAuth("tenant1"),
41+
TestConfig.ForFirebaseAuth().WithEmulator(),
42+
TestConfig.ForTenantAwareFirebaseAuth("tenant1").WithEmulator(),
4343
};
4444

45-
public static readonly IEnumerable<object[]> MainTenantTestConfigs = new List<object[]>()
45+
public static readonly TheoryData<TestConfig> MainTenantTestConfigs = new TheoryData<TestConfig>()
4646
{
47-
new object[] { TestConfig.ForFirebaseAuth() },
48-
new object[] { TestConfig.ForFirebaseAuth().WithEmulator() },
47+
TestConfig.ForFirebaseAuth(),
48+
TestConfig.ForFirebaseAuth().WithEmulator(),
4949
};
5050

5151
private const string CreateUserResponse = @"{""localId"": ""user1""}";
5252

53+
public static TheoryData<TestConfig, string, string> UpdateUserInvalidProviderToLinkTestData
54+
{
55+
get
56+
{
57+
var data = new TheoryData<TestConfig, string, string>();
58+
59+
foreach (var testConfigObj in TestConfigs)
60+
{
61+
var testConfig = (TestConfig)testConfigObj[0];
62+
63+
data.Add(testConfig, "google_user1", string.Empty); // Empty provider ID
64+
data.Add(testConfig, string.Empty, "google.com"); // Empty provider UID
65+
}
66+
67+
return data;
68+
}
69+
}
70+
5371
[Theory]
5472
[MemberData(nameof(TestConfigs))]
5573
public async Task GetUserById(TestConfig config)
@@ -1107,6 +1125,12 @@ public async Task UpdateUser(TestConfig config)
11071125
{ "package", "gold" },
11081126
};
11091127

1128+
var providerToLink = new ProviderUserInfoArgs()
1129+
{
1130+
Uid = "google_user1",
1131+
ProviderId = "google.com",
1132+
};
1133+
11101134
var user = await auth.UpdateUserAsync(new UserRecordArgs()
11111135
{
11121136
CustomClaims = customClaims,
@@ -1118,6 +1142,7 @@ public async Task UpdateUser(TestConfig config)
11181142
PhoneNumber = "+1234567890",
11191143
PhotoUrl = "https://example.com/user.png",
11201144
Uid = "user1",
1145+
ProviderToLink = providerToLink,
11211146
});
11221147

11231148
Assert.Equal("user1", user.Uid);
@@ -1135,6 +1160,13 @@ public async Task UpdateUser(TestConfig config)
11351160
Assert.Equal("+1234567890", request["phoneNumber"]);
11361161
Assert.Equal("https://example.com/user.png", request["photoUrl"]);
11371162

1163+
var expectedProviderUserInfo = new JObject
1164+
{
1165+
{ "Uid", "google_user1" },
1166+
{ "ProviderId", "google.com" },
1167+
};
1168+
Assert.Equal(expectedProviderUserInfo, request["linkProviderUserInfo"]);
1169+
11381170
var claims = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>((string)request["customAttributes"]);
11391171
Assert.True((bool)claims["admin"]);
11401172
Assert.Equal(4L, claims["level"]);
@@ -1168,6 +1200,37 @@ public async Task UpdateUserPartial(TestConfig config)
11681200
Assert.True((bool)request["emailVerified"]);
11691201
}
11701202

1203+
[Theory]
1204+
[MemberData(nameof(TestConfigs))]
1205+
public async Task UpdateUserLinkProvider(TestConfig config)
1206+
{
1207+
var handler = new MockMessageHandler()
1208+
{
1209+
Response = new List<string>() { CreateUserResponse, config.GetUserResponse() },
1210+
};
1211+
var auth = config.CreateAuth(handler);
1212+
1213+
var user = await auth.UpdateUserAsync(new UserRecordArgs()
1214+
{
1215+
Uid = "user1",
1216+
ProviderToLink = new ProviderUserInfoArgs()
1217+
{
1218+
Uid = "google_user1",
1219+
ProviderId = "google.com",
1220+
},
1221+
});
1222+
1223+
Assert.Equal("user1", user.Uid);
1224+
Assert.Equal(2, handler.Requests.Count);
1225+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(handler.Requests[0].Body);
1226+
Assert.Equal(2, request.Count);
1227+
Assert.Equal("user1", request["localId"]);
1228+
var expectedProviderUserInfo = new JObject();
1229+
expectedProviderUserInfo.Add("Uid", "google_user1");
1230+
expectedProviderUserInfo.Add("ProviderId", "google.com");
1231+
Assert.Equal(expectedProviderUserInfo, request["linkProviderUserInfo"]);
1232+
}
1233+
11711234
[Theory]
11721235
[MemberData(nameof(TestConfigs))]
11731236
public async Task UpdateUserRemoveAttributes(TestConfig config)
@@ -1212,6 +1275,7 @@ public async Task UpdateUserRemoveProviders(TestConfig config)
12121275
{
12131276
PhoneNumber = null,
12141277
Uid = "user1",
1278+
ProvidersToDelete = new List<string>() { "google.com" },
12151279
});
12161280

12171281
Assert.Equal("user1", user.Uid);
@@ -1223,7 +1287,7 @@ public async Task UpdateUserRemoveProviders(TestConfig config)
12231287
Assert.Equal(2, request.Count);
12241288
Assert.Equal("user1", request["localId"]);
12251289
Assert.Equal(
1226-
new JArray() { "phone" },
1290+
new JArray() { "phone", "google.com" },
12271291
request["deleteProvider"]);
12281292
}
12291293

@@ -1485,6 +1549,110 @@ public async Task UpdateUserShortPassword(TestConfig config)
14851549
Assert.Empty(handler.Requests);
14861550
}
14871551

1552+
[Theory]
1553+
[MemberData(nameof(UpdateUserInvalidProviderToLinkTestData))]
1554+
public async Task UpdateUserInvalidProviderToLink(TestConfig config, string uid, string providerId)
1555+
{
1556+
var handler = new MockMessageHandler() { Response = CreateUserResponse };
1557+
var auth = config.CreateAuth(handler);
1558+
1559+
var args = new UserRecordArgs()
1560+
{
1561+
ProviderToLink = new ProviderUserInfoArgs()
1562+
{
1563+
Uid = uid,
1564+
ProviderId = providerId,
1565+
},
1566+
Uid = "user1",
1567+
};
1568+
await Assert.ThrowsAsync<ArgumentException>(
1569+
async () => await auth.UpdateUserAsync(args));
1570+
Assert.Empty(handler.Requests);
1571+
}
1572+
1573+
[Theory]
1574+
[MemberData(nameof(TestConfigs))]
1575+
public async Task UpdateUserInvalidEmailProviderToLink(TestConfig config)
1576+
{
1577+
var handler = new MockMessageHandler() { Response = CreateUserResponse };
1578+
var auth = config.CreateAuth(handler);
1579+
1580+
// Phone provider updated in 2 places in the same request
1581+
var args = new UserRecordArgs()
1582+
{
1583+
ProviderToLink = new ProviderUserInfoArgs()
1584+
{
1585+
1586+
ProviderId = "email",
1587+
},
1588+
Uid = "user1",
1589+
Email = "[email protected]",
1590+
};
1591+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
1592+
async () => await auth.UpdateUserAsync(args));
1593+
1594+
const string expectedError = "Both UpdateRequest.Email and UpdateRequest.ProviderToLink.ProviderId='email' " +
1595+
"were set. To link to the email/password provider, only specify the " +
1596+
"UpdateRequest.Email field.";
1597+
1598+
Assert.Equal(expectedError, exception.Message);
1599+
Assert.Empty(handler.Requests);
1600+
}
1601+
1602+
[Theory]
1603+
[MemberData(nameof(TestConfigs))]
1604+
public async Task UpdateUserInvalidPhoneProviderToLink(TestConfig config)
1605+
{
1606+
var handler = new MockMessageHandler() { Response = CreateUserResponse };
1607+
var auth = config.CreateAuth(handler);
1608+
1609+
// Phone provider updated in 2 places in the same request
1610+
var args = new UserRecordArgs()
1611+
{
1612+
ProviderToLink = new ProviderUserInfoArgs()
1613+
{
1614+
Uid = "+11234567891",
1615+
ProviderId = "phone",
1616+
},
1617+
Uid = "user1",
1618+
PhoneNumber = "+11234567891",
1619+
};
1620+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
1621+
async () => await auth.UpdateUserAsync(args));
1622+
1623+
const string expectedError = "Both UpdateRequest.PhoneNumber and UpdateRequest.ProviderToLink.ProviderId='phone'" +
1624+
" were set. To link to a phone provider, only specify the " +
1625+
"UpdateRequest.PhoneNumber field.";
1626+
1627+
Assert.Equal(expectedError, exception.Message);
1628+
Assert.Empty(handler.Requests);
1629+
}
1630+
1631+
[Theory]
1632+
[MemberData(nameof(TestConfigs))]
1633+
public async Task UpdateUserInvalidProvidersToDelete(TestConfig config)
1634+
{
1635+
var handler = new MockMessageHandler() { Response = CreateUserResponse };
1636+
var auth = config.CreateAuth(handler);
1637+
1638+
// Empty provider ID
1639+
var args = new UserRecordArgs()
1640+
{
1641+
ProvidersToDelete = new List<string>() { "google.com", string.Empty },
1642+
Uid = "user1",
1643+
};
1644+
await Assert.ThrowsAsync<ArgumentException>(
1645+
async () => await auth.UpdateUserAsync(args));
1646+
Assert.Empty(handler.Requests);
1647+
1648+
// Phone provider updates in two places
1649+
args.PhoneNumber = null;
1650+
args.ProvidersToDelete = new List<string>() { "google.com", "phone" };
1651+
await Assert.ThrowsAsync<ArgumentException>(
1652+
async () => await auth.UpdateUserAsync(args));
1653+
Assert.Empty(handler.Requests);
1654+
}
1655+
14881656
[Theory]
14891657
[MemberData(nameof(TestConfigs))]
14901658
public void EmptyNameClaims(TestConfig config)

Diff for: FirebaseAdmin/FirebaseAdmin/Auth/ProviderIdentifier.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public sealed class ProviderIdentifier : UserIdentifier
3535
/// <param name="providerUid">The providerUid.</param>
3636
public ProviderIdentifier(string providerId, string providerUid)
3737
{
38-
UserRecordArgs.CheckProvider(providerId, providerUid, required: true);
38+
UserRecordArgs.CheckProvider(providerId, providerUid, true, true);
3939
this.providerId = providerId;
4040
this.providerUid = providerUid;
4141
}

Diff for: FirebaseAdmin/FirebaseAdmin/Auth/ProviderUserInfo.cs

+28-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using FirebaseAdmin.Auth.Users;
16+
using Newtonsoft.Json;
1617

1718
namespace FirebaseAdmin.Auth
1819
{
@@ -36,41 +37,61 @@ internal ProviderUserInfo(GetAccountInfoResponse.Provider provider)
3637
this.ProviderId = provider.ProviderID;
3738
}
3839

40+
/// <summary>
41+
/// Initializes a new instance of the <see cref="ProviderUserInfo"/> class.
42+
/// </summary>
43+
/// <param name="args">Arguments that describe the new provider user info.</param>
44+
internal ProviderUserInfo(ProviderUserInfoArgs args)
45+
{
46+
this.Uid = args.Uid;
47+
this.DisplayName = args.DisplayName;
48+
this.Email = args.Email;
49+
this.PhoneNumber = args.PhoneNumber;
50+
this.PhotoUrl = args.PhotoUrl;
51+
this.ProviderId = args.ProviderId;
52+
}
53+
3954
/// <summary>
4055
/// Gets the user's unique ID assigned by the identity provider.
4156
/// </summary>
4257
/// <returns>a user ID string.</returns>
43-
public string Uid { get; private set; }
58+
[JsonProperty(PropertyName = "rawId")]
59+
public string Uid { get; }
4460

4561
/// <summary>
4662
/// Gets the user's display name, if available.
4763
/// </summary>
4864
/// <returns>a display name string or null.</returns>
49-
public string DisplayName { get; private set; }
65+
[JsonProperty(PropertyName = "displayName")]
66+
public string DisplayName { get; }
5067

5168
/// <summary>
5269
/// Gets the user's email address, if available.
5370
/// </summary>
5471
/// <returns>an email address string or null.</returns>
55-
public string Email { get; private set; }
72+
[JsonProperty(PropertyName = "email")]
73+
public string Email { get; }
5674

5775
/// <summary>
58-
/// Gets the user's phone number.
76+
/// Gets the user's phone number, if available.
5977
/// </summary>
6078
/// <returns>a phone number string or null.</returns>
61-
public string PhoneNumber { get; private set; }
79+
[JsonProperty(PropertyName = "phoneNumber")]
80+
public string PhoneNumber { get; }
6281

6382
/// <summary>
6483
/// Gets the user's photo URL, if available.
6584
/// </summary>
6685
/// <returns>a URL string or null.</returns>
67-
public string PhotoUrl { get; private set; }
86+
[JsonProperty(PropertyName = "photoUrl")]
87+
public string PhotoUrl { get; }
6888

6989
/// <summary>
7090
/// Gets the ID of the identity provider. This can be a short domain name (e.g. google.com) or
7191
/// the identifier of an OpenID identity provider.
7292
/// </summary>
7393
/// <returns>an ID string that uniquely identifies the identity provider.</returns>
74-
public string ProviderId { get; private set; }
94+
[JsonProperty(PropertyName = "providerId")]
95+
public string ProviderId { get; }
7596
}
7697
}
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2023, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace FirebaseAdmin.Auth
16+
{
17+
/// <summary>
18+
/// Contains metadata regarding how a user is known by a particular identity provider (IdP).
19+
/// </summary>
20+
public sealed class ProviderUserInfoArgs
21+
{
22+
/// <summary>
23+
/// Gets or sets the user's unique ID assigned by the identity provider.
24+
/// </summary>
25+
/// <returns>a user ID string.</returns>
26+
public string Uid { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the user's display name, if available.
30+
/// </summary>
31+
/// <returns>a display name string or null.</returns>
32+
public string DisplayName { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the user's email address, if available.
36+
/// </summary>
37+
/// <returns>an email address string or null.</returns>
38+
public string Email { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the user's phone number.
42+
/// </summary>
43+
/// <returns>a phone number string or null.</returns>
44+
public string PhoneNumber { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the user's photo URL, if available.
48+
/// </summary>
49+
/// <returns>a URL string or null.</returns>
50+
public string PhotoUrl { get; set; }
51+
52+
/// <summary>
53+
/// Gets or sets the ID of the identity provider. This can be a short domain name (e.g.
54+
/// google.com) or the identifier of an OpenID identity provider.
55+
/// </summary>
56+
/// <returns>an ID string that uniquely identifies the identity provider.</returns>
57+
public string ProviderId { get; set; }
58+
}
59+
}

0 commit comments

Comments
 (0)