Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ await Assert.ThrowsAsync<InvalidOperationException>(
async () => await auth.CreateCustomTokenAsync("user"));
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await auth.VerifyIdTokenAsync("user"));
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await auth.SetCustomUserClaimsAsync("user", null));
}

[Fact]
Expand Down
80 changes: 43 additions & 37 deletions FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ private FirebaseAuth(FirebaseApp app)
() => FirebaseTokenFactory.Create(this.app), true);
this.idTokenVerifier = new Lazy<FirebaseTokenVerifier>(
() => FirebaseTokenVerifier.CreateIDTokenVerifier(this.app), true);
this.userManager = new Lazy<FirebaseUserManager>(() =>
FirebaseUserManager.Create(this.app));
this.userManager = new Lazy<FirebaseUserManager>(
() => FirebaseUserManager.Create(this.app), true);
}

/// <summary>
Expand Down Expand Up @@ -211,17 +211,7 @@ public async Task<string> CreateCustomTokenAsync(
IDictionary<string, object> 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);
}
Expand Down Expand Up @@ -268,15 +258,8 @@ public async Task<FirebaseToken> VerifyIdTokenAsync(string idToken)
public async Task<FirebaseToken> 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);
}

Expand All @@ -295,22 +278,39 @@ public async Task<FirebaseToken> VerifyIdTokenAsync(
/// <param name="claims">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.</param>
public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary<string, object> claims)
public async Task SetCustomUserClaimsAsync(
string uid, IReadOnlyDictionary<string, object> claims)
{
lock (this.authLock)
{
if (this.deleted)
{
throw new InvalidOperationException("Cannot invoke after deleting the app.");
}
}
await this.SetCustomUserClaimsAsync(uid, claims, default(CancellationToken));
}

/// <summary>
/// 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.
/// </summary>
/// <returns>A task that completes when the claims have been set.</returns>
/// <exception cref="ArgumentException">If <paramref name="uid"/> is null, empty or longer
/// than 128 characters. Or, if the serialized <paramref name="claims"/> is larger than 1000
/// characters.</exception>
/// <param name="uid">The user ID string for the custom claims will be set. Must not be null
/// or longer than 128 characters.
/// </param>
/// <param name="claims">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.</param>
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
/// operation.</param>
public async Task SetCustomUserClaimsAsync(
string uid, IReadOnlyDictionary<string, object> 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);
}

/// <summary>
Expand All @@ -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<TResult>(Func<TResult> func)
{
lock (this.authLock)
{
if (this.deleted)
{
this.userManager.Value.Dispose();
throw new InvalidOperationException("Cannot invoke after deleting the app.");
}

return func();
}
}
}
Expand Down
84 changes: 54 additions & 30 deletions FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -64,50 +65,73 @@ public static FirebaseUserManager Create(FirebaseApp app)
/// </summary>
/// <exception cref="FirebaseException">If the server responds that cannot update the user.</exception>
/// <param name="user">The user which we want to update.</param>
public async Task UpdateUserAsync(UserRecord user)
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
/// operation.</param>
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<JObject>(
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<TResult> PostAndDeserializeAsync<TResult>(
string path, object body, CancellationToken cancellationToken)
{
var json = await this.PostAsync(path, body, cancellationToken).ConfigureAwait(false);
return this.SafeDeserialize<TResult>(json);
}

private TResult SafeDeserialize<TResult>(string json)
{
try
{
var userResponse = resopnse.ToObject<UserRecord>();
if (userResponse.Uid != user.Uid)
{
throw new FirebaseException($"Failed to update user: {user.Uid}");
}
return NewtonsoftJsonSerializer.Instance.Deserialize<TResult>(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<string> 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<JObject> PostAsync(string path, UserRecord user)
private async Task<string> 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)
{
Expand Down
33 changes: 31 additions & 2 deletions FirebaseAdmin/FirebaseAdmin/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,12 +94,26 @@ public static ConfigurableHttpClient CreateAuthorizedHttpClient(
public static async Task<HttpResponseMessage> PostJsonAsync<T>(
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);
}

/// <summary>
/// Serializes the <paramref name="body"/> into JSON, and wraps the result in an instance
/// of <see cref="HttpContent"/>, which can be included in an outgoing HTTP request.
/// </summary>
/// <returns>An instance of <see cref="HttpContent"/> containing the JSON representation
/// of <paramref name="body"/>.</returns>
/// <param name="serializer">The JSON serializer to serialize the given object.</param>
/// <param name="body">The object that will be serialized into JSON.</param>
public static HttpContent CreateJsonHttpContent(
this NewtonsoftJsonSerializer serializer, object body)
{
var payload = serializer.Serialize(body);
return new StringContent(payload, Encoding.UTF8, "application/json");
}

/// <summary>
/// Returns a Unix-styled timestamp (seconds from epoch) from the <see cref="IClock"/>.
/// </summary>
Expand All @@ -110,6 +125,20 @@ public static long UnixTimestamp(this IClock clock)
return (long)timeSinceEpoch.TotalSeconds;
}

/// <summary>
/// Disposes a lazy-initialized object if the object has already been created.
/// </summary>
/// <param name="lazy">The lazy initializer containing a disposable object.</param>
/// <typeparam name="T">Type of the object that needs to be disposed.</typeparam>
public static void DisposeIfCreated<T>(this Lazy<T> lazy)
where T : IDisposable
{
if (lazy.IsValueCreated)
{
lazy.Value.Dispose();
}
}

/// <summary>
/// Creates a shallow copy of a collection of key-value pairs.
/// </summary>
Expand Down