diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs index 491aa9af..0f6ee536 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs @@ -69,6 +69,8 @@ await Assert.ThrowsAsync( async () => await auth.CreateCustomTokenAsync("user")); await Assert.ThrowsAsync( async () => await auth.VerifyIdTokenAsync("user")); + await Assert.ThrowsAsync( + async () => await auth.SetCustomUserClaimsAsync("user", null)); } [Fact] diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index a07419db..cd3827ef 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -39,8 +39,8 @@ private FirebaseAuth(FirebaseApp app) () => FirebaseTokenFactory.Create(this.app), true); this.idTokenVerifier = new Lazy( () => FirebaseTokenVerifier.CreateIDTokenVerifier(this.app), true); - this.userManager = new Lazy(() => - FirebaseUserManager.Create(this.app)); + this.userManager = new Lazy( + () => FirebaseUserManager.Create(this.app), true); } /// @@ -211,17 +211,7 @@ public async Task CreateCustomTokenAsync( IDictionary developerClaims, CancellationToken cancellationToken) { - FirebaseTokenFactory tokenFactory; - lock (this.authLock) - { - if (this.deleted) - { - throw new InvalidOperationException("Cannot invoke after deleting the app."); - } - - tokenFactory = this.tokenFactory.Value; - } - + var tokenFactory = this.IfNotDeleted(() => this.tokenFactory.Value); return await tokenFactory.CreateCustomTokenAsync( uid, developerClaims, cancellationToken).ConfigureAwait(false); } @@ -268,15 +258,8 @@ public async Task VerifyIdTokenAsync(string idToken) public async Task VerifyIdTokenAsync( string idToken, CancellationToken cancellationToken) { - lock (this.authLock) - { - if (this.deleted) - { - throw new InvalidOperationException("Cannot invoke after deleting the app."); - } - } - - return await this.idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken) + var idTokenVerifier = this.IfNotDeleted(() => this.idTokenVerifier.Value); + return await idTokenVerifier.VerifyTokenAsync(idToken, cancellationToken) .ConfigureAwait(false); } @@ -295,22 +278,39 @@ public async Task VerifyIdTokenAsync( /// The claims to be stored on the user account, and made /// available to Firebase security rules. These must be serializable to JSON, and the /// serialized claims should not be larger than 1000 characters. - public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary claims) + public async Task SetCustomUserClaimsAsync( + string uid, IReadOnlyDictionary claims) { - lock (this.authLock) - { - if (this.deleted) - { - throw new InvalidOperationException("Cannot invoke after deleting the app."); - } - } + await this.SetCustomUserClaimsAsync(uid, claims, default(CancellationToken)); + } + /// + /// Sets the specified custom claims on an existing user account. A null claims value + /// removes any claims currently set on the user account. The claims should serialize into + /// a valid JSON string. The serialized claims must not be larger than 1000 characters. + /// + /// A task that completes when the claims have been set. + /// If is null, empty or longer + /// than 128 characters. Or, if the serialized is larger than 1000 + /// characters. + /// The user ID string for the custom claims will be set. Must not be null + /// or longer than 128 characters. + /// + /// The claims to be stored on the user account, and made + /// available to Firebase security rules. These must be serializable to JSON, and after + /// serialization it should not be larger than 1000 characters. + /// A cancellation token to monitor the asynchronous + /// operation. + public async Task SetCustomUserClaimsAsync( + string uid, IReadOnlyDictionary claims, CancellationToken cancellationToken) + { + var userManager = this.IfNotDeleted(() => this.userManager.Value); var user = new UserRecord(uid) { CustomClaims = claims, }; - await this.userManager.Value.UpdateUserAsync(user); + await userManager.UpdateUserAsync(user, cancellationToken).ConfigureAwait(false); } /// @@ -321,15 +321,21 @@ void IFirebaseService.Delete() lock (this.authLock) { this.deleted = true; - if (this.tokenFactory.IsValueCreated) - { - this.tokenFactory.Value.Dispose(); - } + this.tokenFactory.DisposeIfCreated(); + this.userManager.DisposeIfCreated(); + } + } - if (this.userManager.IsValueCreated) + private TResult IfNotDeleted(Func func) + { + lock (this.authLock) + { + if (this.deleted) { - this.userManager.Value.Dispose(); + throw new InvalidOperationException("Cannot invoke after deleting the app."); } + + return func(); } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs index 805d6347..75f53137 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs @@ -14,9 +14,11 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Google.Apis.Auth.OAuth2; using Google.Apis.Http; +using Google.Apis.Json; using Newtonsoft.Json.Linq; namespace FirebaseAdmin.Auth @@ -36,24 +38,23 @@ internal class FirebaseUserManager : IDisposable internal FirebaseUserManager(FirebaseUserManagerArgs args) { + if (string.IsNullOrEmpty(args.ProjectId)) + { + throw new ArgumentException( + "Must initialize FirebaseApp with a project ID to manage users."); + } + this.httpClient = args.ClientFactory.CreateAuthorizedHttpClient(args.Credential); this.baseUrl = string.Format(IdTooklitUrl, args.ProjectId); } public static FirebaseUserManager Create(FirebaseApp app) { - var projectId = app.GetProjectId(); - if (string.IsNullOrEmpty(projectId)) - { - throw new ArgumentException( - "Must initialize FirebaseApp with a project ID to manage users."); - } - var args = new FirebaseUserManagerArgs { ClientFactory = new HttpClientFactory(), Credential = app.Options.Credential, - ProjectId = projectId, + ProjectId = app.GetProjectId(), }; return new FirebaseUserManager(args); @@ -64,50 +65,73 @@ public static FirebaseUserManager Create(FirebaseApp app) /// /// If the server responds that cannot update the user. /// The user which we want to update. - public async Task UpdateUserAsync(UserRecord user) + /// A cancellation token to monitor the asynchronous + /// operation. + public async Task UpdateUserAsync( + UserRecord user, CancellationToken cancellationToken = default(CancellationToken)) { - var updatePath = "/accounts:update"; - var resopnse = await this.PostAsync(updatePath, user); + const string updatePath = "accounts:update"; + var response = await this.PostAndDeserializeAsync( + updatePath, user, cancellationToken).ConfigureAwait(false); + if (user.Uid != (string)response["localId"]) + { + throw new FirebaseException($"Failed to update user: {user.Uid}"); + } + } + public void Dispose() + { + this.httpClient.Dispose(); + } + + private async Task PostAndDeserializeAsync( + string path, object body, CancellationToken cancellationToken) + { + var json = await this.PostAsync(path, body, cancellationToken).ConfigureAwait(false); + return this.SafeDeserialize(json); + } + + private TResult SafeDeserialize(string json) + { try { - var userResponse = resopnse.ToObject(); - if (userResponse.Uid != user.Uid) - { - throw new FirebaseException($"Failed to update user: {user.Uid}"); - } + return NewtonsoftJsonSerializer.Instance.Deserialize(json); } catch (Exception e) { - throw new FirebaseException("Error while calling Firebase Auth service", e); + throw new FirebaseException("Error while parsing Auth service response", e); } } - public void Dispose() + private async Task PostAsync( + string path, object body, CancellationToken cancellationToken) { - this.httpClient.Dispose(); + var request = new HttpRequestMessage() + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{this.baseUrl}/{path}"), + Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body), + }; + return await this.SendAsync(request, cancellationToken).ConfigureAwait(false); } - private async Task PostAsync(string path, UserRecord user) + private async Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) { - var requestUri = $"{this.baseUrl}{path}"; - HttpResponseMessage response = null; try { - response = await this.httpClient.PostJsonAsync(requestUri, user, default); - var json = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - return JObject.Parse(json); - } - else + var response = await this.httpClient.SendAsync(request, cancellationToken) + .ConfigureAwait(false); + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) { var error = "Response status code does not indicate success: " + $"{(int)response.StatusCode} ({response.StatusCode})" + $"{Environment.NewLine}{json}"; throw new FirebaseException(error); } + + return json; } catch (HttpRequestException e) { diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index d8d6f354..b83fe9cd 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; using Google.Apis.Auth.OAuth2; @@ -93,12 +94,26 @@ public static ConfigurableHttpClient CreateAuthorizedHttpClient( public static async Task PostJsonAsync( this HttpClient client, string requestUri, T body, CancellationToken cancellationToken) { - var payload = NewtonsoftJsonSerializer.Instance.Serialize(body); - var content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json"); + var content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body); return await client.PostAsync(requestUri, content, cancellationToken) .ConfigureAwait(false); } + /// + /// Serializes the into JSON, and wraps the result in an instance + /// of , which can be included in an outgoing HTTP request. + /// + /// An instance of containing the JSON representation + /// of . + /// The JSON serializer to serialize the given object. + /// The object that will be serialized into JSON. + public static HttpContent CreateJsonHttpContent( + this NewtonsoftJsonSerializer serializer, object body) + { + var payload = serializer.Serialize(body); + return new StringContent(payload, Encoding.UTF8, "application/json"); + } + /// /// Returns a Unix-styled timestamp (seconds from epoch) from the . /// @@ -110,6 +125,20 @@ public static long UnixTimestamp(this IClock clock) return (long)timeSinceEpoch.TotalSeconds; } + /// + /// Disposes a lazy-initialized object if the object has already been created. + /// + /// The lazy initializer containing a disposable object. + /// Type of the object that needs to be disposed. + public static void DisposeIfCreated(this Lazy lazy) + where T : IDisposable + { + if (lazy.IsValueCreated) + { + lazy.Value.Dispose(); + } + } + /// /// Creates a shallow copy of a collection of key-value pairs. ///