Skip to content

Commit

Permalink
Add the ability to link/unlink a provider user id to an account.
Browse files Browse the repository at this point in the history
  • Loading branch information
nrsim authored and Michael Sundqvist committed Jan 11, 2024
1 parent e4f5a55 commit 9898201
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,40 @@ namespace FirebaseAdmin.Auth.Users.Tests
{
public class FirebaseUserManagerTest
{
public static readonly IEnumerable<object[]> TestConfigs = new List<object[]>()
public static readonly TheoryData<TestConfig> TestConfigs = new TheoryData<TestConfig>()
{
new object[] { TestConfig.ForFirebaseAuth() },
new object[] { TestConfig.ForTenantAwareFirebaseAuth("tenant1") },
new object[] { TestConfig.ForFirebaseAuth().WithEmulator() },
new object[] { TestConfig.ForTenantAwareFirebaseAuth("tenant1").WithEmulator() },
TestConfig.ForFirebaseAuth(),
TestConfig.ForTenantAwareFirebaseAuth("tenant1"),
TestConfig.ForFirebaseAuth().WithEmulator(),
TestConfig.ForTenantAwareFirebaseAuth("tenant1").WithEmulator(),
};

public static readonly IEnumerable<object[]> MainTenantTestConfigs = new List<object[]>()
public static readonly TheoryData<TestConfig> MainTenantTestConfigs = new TheoryData<TestConfig>()
{
new object[] { TestConfig.ForFirebaseAuth() },
new object[] { TestConfig.ForFirebaseAuth().WithEmulator() },
TestConfig.ForFirebaseAuth(),
TestConfig.ForFirebaseAuth().WithEmulator(),
};

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

public static TheoryData<TestConfig, string, string> UpdateUserInvalidProviderToLinkTestData
{
get
{
var data = new TheoryData<TestConfig, string, string>();

foreach (var testConfigObj in TestConfigs)
{
var testConfig = (TestConfig)testConfigObj[0];

data.Add(testConfig, "google_user1", string.Empty); // Empty provider ID
data.Add(testConfig, string.Empty, "google.com"); // Empty provider UID
}

return data;
}
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task GetUserById(TestConfig config)
Expand Down Expand Up @@ -1107,6 +1125,12 @@ public async Task UpdateUser(TestConfig config)
{ "package", "gold" },
};

var providerToLink = new ProviderUserInfoArgs()
{
Uid = "google_user1",
ProviderId = "google.com",
};

var user = await auth.UpdateUserAsync(new UserRecordArgs()
{
CustomClaims = customClaims,
Expand All @@ -1118,6 +1142,7 @@ public async Task UpdateUser(TestConfig config)
PhoneNumber = "+1234567890",
PhotoUrl = "https://example.com/user.png",
Uid = "user1",
ProviderToLink = providerToLink,
});

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

var expectedProviderUserInfo = new JObject
{
{ "Uid", "google_user1" },
{ "ProviderId", "google.com" },
};
Assert.Equal(expectedProviderUserInfo, request["linkProviderUserInfo"]);

var claims = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>((string)request["customAttributes"]);
Assert.True((bool)claims["admin"]);
Assert.Equal(4L, claims["level"]);
Expand Down Expand Up @@ -1168,6 +1200,37 @@ public async Task UpdateUserPartial(TestConfig config)
Assert.True((bool)request["emailVerified"]);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserLinkProvider(TestConfig config)
{
var handler = new MockMessageHandler()
{
Response = new List<string>() { CreateUserResponse, config.GetUserResponse() },
};
var auth = config.CreateAuth(handler);

var user = await auth.UpdateUserAsync(new UserRecordArgs()
{
Uid = "user1",
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = "google_user1",
ProviderId = "google.com",
},
});

Assert.Equal("user1", user.Uid);
Assert.Equal(2, handler.Requests.Count);
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(handler.Requests[0].Body);
Assert.Equal(2, request.Count);
Assert.Equal("user1", request["localId"]);
var expectedProviderUserInfo = new JObject();
expectedProviderUserInfo.Add("Uid", "google_user1");
expectedProviderUserInfo.Add("ProviderId", "google.com");
Assert.Equal(expectedProviderUserInfo, request["linkProviderUserInfo"]);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserRemoveAttributes(TestConfig config)
Expand Down Expand Up @@ -1212,6 +1275,7 @@ public async Task UpdateUserRemoveProviders(TestConfig config)
{
PhoneNumber = null,
Uid = "user1",
ProvidersToDelete = new List<string>() { "google.com" },
});

Assert.Equal("user1", user.Uid);
Expand All @@ -1223,7 +1287,7 @@ public async Task UpdateUserRemoveProviders(TestConfig config)
Assert.Equal(2, request.Count);
Assert.Equal("user1", request["localId"]);
Assert.Equal(
new JArray() { "phone" },
new JArray() { "phone", "google.com" },
request["deleteProvider"]);
}

Expand Down Expand Up @@ -1485,6 +1549,110 @@ public async Task UpdateUserShortPassword(TestConfig config)
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(UpdateUserInvalidProviderToLinkTestData))]
public async Task UpdateUserInvalidProviderToLink(TestConfig config, string uid, string providerId)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

var args = new UserRecordArgs()
{
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = uid,
ProviderId = providerId,
},
Uid = "user1",
};
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.UpdateUserAsync(args));
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserInvalidEmailProviderToLink(TestConfig config)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

// Phone provider updated in 2 places in the same request
var args = new UserRecordArgs()
{
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = "[email protected]",
ProviderId = "email",
},
Uid = "user1",
Email = "[email protected]",
};
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
async () => await auth.UpdateUserAsync(args));

const string expectedError = "Both UpdateRequest.Email and UpdateRequest.ProviderToLink.ProviderId='email' " +
"were set. To link to the email/password provider, only specify the " +
"UpdateRequest.Email field.";

Assert.Equal(expectedError, exception.Message);
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserInvalidPhoneProviderToLink(TestConfig config)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

// Phone provider updated in 2 places in the same request
var args = new UserRecordArgs()
{
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = "+11234567891",
ProviderId = "phone",
},
Uid = "user1",
PhoneNumber = "+11234567891",
};
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
async () => await auth.UpdateUserAsync(args));

const string expectedError = "Both UpdateRequest.PhoneNumber and UpdateRequest.ProviderToLink.ProviderId='phone'" +
" were set. To link to a phone provider, only specify the " +
"UpdateRequest.PhoneNumber field.";

Assert.Equal(expectedError, exception.Message);
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserInvalidProvidersToDelete(TestConfig config)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

// Empty provider ID
var args = new UserRecordArgs()
{
ProvidersToDelete = new List<string>() { "google.com", string.Empty },
Uid = "user1",
};
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.UpdateUserAsync(args));
Assert.Empty(handler.Requests);

// Phone provider updates in two places
args.PhoneNumber = null;
args.ProvidersToDelete = new List<string>() { "google.com", "phone" };
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.UpdateUserAsync(args));
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public void EmptyNameClaims(TestConfig config)
Expand Down
2 changes: 1 addition & 1 deletion FirebaseAdmin/FirebaseAdmin/Auth/ProviderIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public sealed class ProviderIdentifier : UserIdentifier
/// <param name="providerUid">The providerUid.</param>
public ProviderIdentifier(string providerId, string providerUid)
{
UserRecordArgs.CheckProvider(providerId, providerUid, required: true);
UserRecordArgs.CheckProvider(providerId, providerUid, true, true);
this.providerId = providerId;
this.providerUid = providerUid;
}
Expand Down
35 changes: 28 additions & 7 deletions FirebaseAdmin/FirebaseAdmin/Auth/ProviderUserInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using FirebaseAdmin.Auth.Users;
using Newtonsoft.Json;

namespace FirebaseAdmin.Auth
{
Expand All @@ -36,41 +37,61 @@ internal ProviderUserInfo(GetAccountInfoResponse.Provider provider)
this.ProviderId = provider.ProviderID;
}

/// <summary>
/// Initializes a new instance of the <see cref="ProviderUserInfo"/> class.
/// </summary>
/// <param name="args">Arguments that describe the new provider user info.</param>
internal ProviderUserInfo(ProviderUserInfoArgs args)
{
this.Uid = args.Uid;
this.DisplayName = args.DisplayName;
this.Email = args.Email;
this.PhoneNumber = args.PhoneNumber;
this.PhotoUrl = args.PhotoUrl;
this.ProviderId = args.ProviderId;
}

/// <summary>
/// Gets the user's unique ID assigned by the identity provider.
/// </summary>
/// <returns>a user ID string.</returns>
public string Uid { get; private set; }
[JsonProperty(PropertyName = "rawId")]
public string Uid { get; }

/// <summary>
/// Gets the user's display name, if available.
/// </summary>
/// <returns>a display name string or null.</returns>
public string DisplayName { get; private set; }
[JsonProperty(PropertyName = "displayName")]
public string DisplayName { get; }

/// <summary>
/// Gets the user's email address, if available.
/// </summary>
/// <returns>an email address string or null.</returns>
public string Email { get; private set; }
[JsonProperty(PropertyName = "email")]
public string Email { get; }

/// <summary>
/// Gets the user's phone number.
/// Gets the user's phone number, if available.
/// </summary>
/// <returns>a phone number string or null.</returns>
public string PhoneNumber { get; private set; }
[JsonProperty(PropertyName = "phoneNumber")]
public string PhoneNumber { get; }

/// <summary>
/// Gets the user's photo URL, if available.
/// </summary>
/// <returns>a URL string or null.</returns>
public string PhotoUrl { get; private set; }
[JsonProperty(PropertyName = "photoUrl")]
public string PhotoUrl { get; }

/// <summary>
/// Gets the ID of the identity provider. This can be a short domain name (e.g. google.com) or
/// the identifier of an OpenID identity provider.
/// </summary>
/// <returns>an ID string that uniquely identifies the identity provider.</returns>
public string ProviderId { get; private set; }
[JsonProperty(PropertyName = "providerId")]
public string ProviderId { get; }
}
}
59 changes: 59 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/ProviderUserInfoArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2023, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace FirebaseAdmin.Auth
{
/// <summary>
/// Contains metadata regarding how a user is known by a particular identity provider (IdP).
/// </summary>
public sealed class ProviderUserInfoArgs
{
/// <summary>
/// Gets or sets the user's unique ID assigned by the identity provider.
/// </summary>
/// <returns>a user ID string.</returns>
public string Uid { get; set; }

/// <summary>
/// Gets or sets the user's display name, if available.
/// </summary>
/// <returns>a display name string or null.</returns>
public string DisplayName { get; set; }

/// <summary>
/// Gets or sets the user's email address, if available.
/// </summary>
/// <returns>an email address string or null.</returns>
public string Email { get; set; }

/// <summary>
/// Gets or sets the user's phone number.
/// </summary>
/// <returns>a phone number string or null.</returns>
public string PhoneNumber { get; set; }

/// <summary>
/// Gets or sets the user's photo URL, if available.
/// </summary>
/// <returns>a URL string or null.</returns>
public string PhotoUrl { get; set; }

/// <summary>
/// Gets or sets the ID of the identity provider. This can be a short domain name (e.g.
/// google.com) or the identifier of an OpenID identity provider.
/// </summary>
/// <returns>an ID string that uniquely identifies the identity provider.</returns>
public string ProviderId { get; set; }
}
}
Loading

0 comments on commit 9898201

Please sign in to comment.