Skip to content

Commit cec957a

Browse files
authored
Merge pull request #24 from firebase/hkj-init-resource
Refactoring the user management code
2 parents 7aaf52f + aa2bfe2 commit cec957a

File tree

4 files changed

+130
-69
lines changed

4 files changed

+130
-69
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ await Assert.ThrowsAsync<InvalidOperationException>(
6969
async () => await auth.CreateCustomTokenAsync("user"));
7070
await Assert.ThrowsAsync<InvalidOperationException>(
7171
async () => await auth.VerifyIdTokenAsync("user"));
72+
await Assert.ThrowsAsync<InvalidOperationException>(
73+
async () => await auth.SetCustomUserClaimsAsync("user", null));
7274
}
7375

7476
[Fact]

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ private FirebaseAuth(FirebaseApp app)
3939
() => FirebaseTokenFactory.Create(this.app), true);
4040
this.idTokenVerifier = new Lazy<FirebaseTokenVerifier>(
4141
() => FirebaseTokenVerifier.CreateIDTokenVerifier(this.app), true);
42-
this.userManager = new Lazy<FirebaseUserManager>(() =>
43-
FirebaseUserManager.Create(this.app));
42+
this.userManager = new Lazy<FirebaseUserManager>(
43+
() => FirebaseUserManager.Create(this.app), true);
4444
}
4545

4646
/// <summary>
@@ -211,17 +211,7 @@ public async Task<string> CreateCustomTokenAsync(
211211
IDictionary<string, object> developerClaims,
212212
CancellationToken cancellationToken)
213213
{
214-
FirebaseTokenFactory tokenFactory;
215-
lock (this.authLock)
216-
{
217-
if (this.deleted)
218-
{
219-
throw new InvalidOperationException("Cannot invoke after deleting the app.");
220-
}
221-
222-
tokenFactory = this.tokenFactory.Value;
223-
}
224-
214+
var tokenFactory = this.IfNotDeleted(() => this.tokenFactory.Value);
225215
return await tokenFactory.CreateCustomTokenAsync(
226216
uid, developerClaims, cancellationToken).ConfigureAwait(false);
227217
}
@@ -268,15 +258,8 @@ public async Task<FirebaseToken> VerifyIdTokenAsync(string idToken)
268258
public async Task<FirebaseToken> VerifyIdTokenAsync(
269259
string idToken, CancellationToken cancellationToken)
270260
{
271-
lock (this.authLock)
272-
{
273-
if (this.deleted)
274-
{
275-
throw new InvalidOperationException("Cannot invoke after deleting the app.");
276-
}
277-
}
278-
279-
return await this.idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken)
261+
var idTokenVerifier = this.IfNotDeleted(() => this.idTokenVerifier.Value);
262+
return await idTokenVerifier.VerifyTokenAsync(idToken, cancellationToken)
280263
.ConfigureAwait(false);
281264
}
282265

@@ -295,22 +278,39 @@ public async Task<FirebaseToken> VerifyIdTokenAsync(
295278
/// <param name="claims">The claims to be stored on the user account, and made
296279
/// available to Firebase security rules. These must be serializable to JSON, and the
297280
/// serialized claims should not be larger than 1000 characters.</param>
298-
public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary<string, object> claims)
281+
public async Task SetCustomUserClaimsAsync(
282+
string uid, IReadOnlyDictionary<string, object> claims)
299283
{
300-
lock (this.authLock)
301-
{
302-
if (this.deleted)
303-
{
304-
throw new InvalidOperationException("Cannot invoke after deleting the app.");
305-
}
306-
}
284+
await this.SetCustomUserClaimsAsync(uid, claims, default(CancellationToken));
285+
}
307286

287+
/// <summary>
288+
/// Sets the specified custom claims on an existing user account. A null claims value
289+
/// removes any claims currently set on the user account. The claims should serialize into
290+
/// a valid JSON string. The serialized claims must not be larger than 1000 characters.
291+
/// </summary>
292+
/// <returns>A task that completes when the claims have been set.</returns>
293+
/// <exception cref="ArgumentException">If <paramref name="uid"/> is null, empty or longer
294+
/// than 128 characters. Or, if the serialized <paramref name="claims"/> is larger than 1000
295+
/// characters.</exception>
296+
/// <param name="uid">The user ID string for the custom claims will be set. Must not be null
297+
/// or longer than 128 characters.
298+
/// </param>
299+
/// <param name="claims">The claims to be stored on the user account, and made
300+
/// available to Firebase security rules. These must be serializable to JSON, and after
301+
/// serialization it should not be larger than 1000 characters.</param>
302+
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
303+
/// operation.</param>
304+
public async Task SetCustomUserClaimsAsync(
305+
string uid, IReadOnlyDictionary<string, object> claims, CancellationToken cancellationToken)
306+
{
307+
var userManager = this.IfNotDeleted(() => this.userManager.Value);
308308
var user = new UserRecord(uid)
309309
{
310310
CustomClaims = claims,
311311
};
312312

313-
await this.userManager.Value.UpdateUserAsync(user);
313+
await userManager.UpdateUserAsync(user, cancellationToken).ConfigureAwait(false);
314314
}
315315

316316
/// <summary>
@@ -321,15 +321,21 @@ void IFirebaseService.Delete()
321321
lock (this.authLock)
322322
{
323323
this.deleted = true;
324-
if (this.tokenFactory.IsValueCreated)
325-
{
326-
this.tokenFactory.Value.Dispose();
327-
}
324+
this.tokenFactory.DisposeIfCreated();
325+
this.userManager.DisposeIfCreated();
326+
}
327+
}
328328

329-
if (this.userManager.IsValueCreated)
329+
private TResult IfNotDeleted<TResult>(Func<TResult> func)
330+
{
331+
lock (this.authLock)
332+
{
333+
if (this.deleted)
330334
{
331-
this.userManager.Value.Dispose();
335+
throw new InvalidOperationException("Cannot invoke after deleting the app.");
332336
}
337+
338+
return func();
333339
}
334340
}
335341
}

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
using System;
1616
using System.Net.Http;
17+
using System.Threading;
1718
using System.Threading.Tasks;
1819
using Google.Apis.Auth.OAuth2;
1920
using Google.Apis.Http;
21+
using Google.Apis.Json;
2022
using Newtonsoft.Json.Linq;
2123

2224
namespace FirebaseAdmin.Auth
@@ -36,24 +38,23 @@ internal class FirebaseUserManager : IDisposable
3638

3739
internal FirebaseUserManager(FirebaseUserManagerArgs args)
3840
{
41+
if (string.IsNullOrEmpty(args.ProjectId))
42+
{
43+
throw new ArgumentException(
44+
"Must initialize FirebaseApp with a project ID to manage users.");
45+
}
46+
3947
this.httpClient = args.ClientFactory.CreateAuthorizedHttpClient(args.Credential);
4048
this.baseUrl = string.Format(IdTooklitUrl, args.ProjectId);
4149
}
4250

4351
public static FirebaseUserManager Create(FirebaseApp app)
4452
{
45-
var projectId = app.GetProjectId();
46-
if (string.IsNullOrEmpty(projectId))
47-
{
48-
throw new ArgumentException(
49-
"Must initialize FirebaseApp with a project ID to manage users.");
50-
}
51-
5253
var args = new FirebaseUserManagerArgs
5354
{
5455
ClientFactory = new HttpClientFactory(),
5556
Credential = app.Options.Credential,
56-
ProjectId = projectId,
57+
ProjectId = app.GetProjectId(),
5758
};
5859

5960
return new FirebaseUserManager(args);
@@ -64,50 +65,73 @@ public static FirebaseUserManager Create(FirebaseApp app)
6465
/// </summary>
6566
/// <exception cref="FirebaseException">If the server responds that cannot update the user.</exception>
6667
/// <param name="user">The user which we want to update.</param>
67-
public async Task UpdateUserAsync(UserRecord user)
68+
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
69+
/// operation.</param>
70+
public async Task UpdateUserAsync(
71+
UserRecord user, CancellationToken cancellationToken = default(CancellationToken))
6872
{
69-
var updatePath = "/accounts:update";
70-
var resopnse = await this.PostAsync(updatePath, user);
73+
const string updatePath = "accounts:update";
74+
var response = await this.PostAndDeserializeAsync<JObject>(
75+
updatePath, user, cancellationToken).ConfigureAwait(false);
76+
if (user.Uid != (string)response["localId"])
77+
{
78+
throw new FirebaseException($"Failed to update user: {user.Uid}");
79+
}
80+
}
7181

82+
public void Dispose()
83+
{
84+
this.httpClient.Dispose();
85+
}
86+
87+
private async Task<TResult> PostAndDeserializeAsync<TResult>(
88+
string path, object body, CancellationToken cancellationToken)
89+
{
90+
var json = await this.PostAsync(path, body, cancellationToken).ConfigureAwait(false);
91+
return this.SafeDeserialize<TResult>(json);
92+
}
93+
94+
private TResult SafeDeserialize<TResult>(string json)
95+
{
7296
try
7397
{
74-
var userResponse = resopnse.ToObject<UserRecord>();
75-
if (userResponse.Uid != user.Uid)
76-
{
77-
throw new FirebaseException($"Failed to update user: {user.Uid}");
78-
}
98+
return NewtonsoftJsonSerializer.Instance.Deserialize<TResult>(json);
7999
}
80100
catch (Exception e)
81101
{
82-
throw new FirebaseException("Error while calling Firebase Auth service", e);
102+
throw new FirebaseException("Error while parsing Auth service response", e);
83103
}
84104
}
85105

86-
public void Dispose()
106+
private async Task<string> PostAsync(
107+
string path, object body, CancellationToken cancellationToken)
87108
{
88-
this.httpClient.Dispose();
109+
var request = new HttpRequestMessage()
110+
{
111+
Method = HttpMethod.Post,
112+
RequestUri = new Uri($"{this.baseUrl}/{path}"),
113+
Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body),
114+
};
115+
return await this.SendAsync(request, cancellationToken).ConfigureAwait(false);
89116
}
90117

91-
private async Task<JObject> PostAsync(string path, UserRecord user)
118+
private async Task<string> SendAsync(
119+
HttpRequestMessage request, CancellationToken cancellationToken)
92120
{
93-
var requestUri = $"{this.baseUrl}{path}";
94-
HttpResponseMessage response = null;
95121
try
96122
{
97-
response = await this.httpClient.PostJsonAsync(requestUri, user, default);
98-
var json = await response.Content.ReadAsStringAsync();
99-
100-
if (response.IsSuccessStatusCode)
101-
{
102-
return JObject.Parse(json);
103-
}
104-
else
123+
var response = await this.httpClient.SendAsync(request, cancellationToken)
124+
.ConfigureAwait(false);
125+
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
126+
if (!response.IsSuccessStatusCode)
105127
{
106128
var error = "Response status code does not indicate success: "
107129
+ $"{(int)response.StatusCode} ({response.StatusCode})"
108130
+ $"{Environment.NewLine}{json}";
109131
throw new FirebaseException(error);
110132
}
133+
134+
return json;
111135
}
112136
catch (HttpRequestException e)
113137
{

FirebaseAdmin/FirebaseAdmin/Extensions.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System;
1616
using System.Collections.Generic;
1717
using System.Net.Http;
18+
using System.Text;
1819
using System.Threading;
1920
using System.Threading.Tasks;
2021
using Google.Apis.Auth.OAuth2;
@@ -93,12 +94,26 @@ public static ConfigurableHttpClient CreateAuthorizedHttpClient(
9394
public static async Task<HttpResponseMessage> PostJsonAsync<T>(
9495
this HttpClient client, string requestUri, T body, CancellationToken cancellationToken)
9596
{
96-
var payload = NewtonsoftJsonSerializer.Instance.Serialize(body);
97-
var content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
97+
var content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body);
9898
return await client.PostAsync(requestUri, content, cancellationToken)
9999
.ConfigureAwait(false);
100100
}
101101

102+
/// <summary>
103+
/// Serializes the <paramref name="body"/> into JSON, and wraps the result in an instance
104+
/// of <see cref="HttpContent"/>, which can be included in an outgoing HTTP request.
105+
/// </summary>
106+
/// <returns>An instance of <see cref="HttpContent"/> containing the JSON representation
107+
/// of <paramref name="body"/>.</returns>
108+
/// <param name="serializer">The JSON serializer to serialize the given object.</param>
109+
/// <param name="body">The object that will be serialized into JSON.</param>
110+
public static HttpContent CreateJsonHttpContent(
111+
this NewtonsoftJsonSerializer serializer, object body)
112+
{
113+
var payload = serializer.Serialize(body);
114+
return new StringContent(payload, Encoding.UTF8, "application/json");
115+
}
116+
102117
/// <summary>
103118
/// Returns a Unix-styled timestamp (seconds from epoch) from the <see cref="IClock"/>.
104119
/// </summary>
@@ -110,6 +125,20 @@ public static long UnixTimestamp(this IClock clock)
110125
return (long)timeSinceEpoch.TotalSeconds;
111126
}
112127

128+
/// <summary>
129+
/// Disposes a lazy-initialized object if the object has already been created.
130+
/// </summary>
131+
/// <param name="lazy">The lazy initializer containing a disposable object.</param>
132+
/// <typeparam name="T">Type of the object that needs to be disposed.</typeparam>
133+
public static void DisposeIfCreated<T>(this Lazy<T> lazy)
134+
where T : IDisposable
135+
{
136+
if (lazy.IsValueCreated)
137+
{
138+
lazy.Value.Dispose();
139+
}
140+
}
141+
113142
/// <summary>
114143
/// Creates a shallow copy of a collection of key-value pairs.
115144
/// </summary>

0 commit comments

Comments
 (0)