Firebase Multitenancy & .NET 7
Serhii Kokhan
Microsoft MVP??CTO & .NET/Azure Architect??Stripe Certified Professional Developer??Offering MVP Development, Legacy Migration, & Product Engineering??Expert in scalable solutions, high-load systems, and API integrations
Introduction
Firebase is a leading platform for developing mobile and web applications, offering a variety of tools and services to make development straightforward and efficient. It’s filled with features that make creating, deploying, and managing applications a breeze, becoming a vital resource for developers who want reliable and efficient tools. One important feature of Firebase is multitenancy, allowing developers to build versatile applications that can serve different groups, each with their own set of data and functionality.
With multitenancy in Firebase, you can build scalable solutions that can serve varied users without sacrificing data integrity and user experience. It lets you create individual environments within your app to make sure each user group has a secure and personalized experience.
In this article, we will explore how to implement multitenancy in Firebase using .NET 7. We will look into the essential principles and advanced approaches for integrating multitenancy in the .NET environment effectively. We’ll use the Carcass framework, a creative, community-supported, open-source infrastructure for building modern and scalable .NET 7 applications. Whether you're an experienced developer or just starting, this article will provide the knowledge and insights you need to utilize Firebase multitenancy effectively in .NET 7 development.
Prerequisites
Before diving into Firebase multitenancy, make sure you have created a Firebase project with a configured Firebase Authentication.
Overview of Implementation
FirebaseAuthenticationHandler
The FirebaseAuthenticationHandler class is crucial for ensuring the security and integrity of the requests, it’s responsible for managing authentication processes.
public sealed class FirebaseAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private const string BearerPrefix = "Bearer ";
private readonly FirebaseApp _firebaseApp;
public FirebaseAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
FirebaseApp firebaseApp
) : base(options, logger, encoder, clock)
{
ArgumentVerifier.NotNull(firebaseApp, nameof(firebaseApp));
_firebaseApp = firebaseApp;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.NoResult();
string? authorizationHeaderValue = Context.Request.Headers["Authorization"];
if (authorizationHeaderValue is null || !authorizationHeaderValue.StartsWith(BearerPrefix))
return AuthenticateResult.Fail("Invalid scheme.");
string idToken = authorizationHeaderValue[BearerPrefix.Length..];
try
{
FirebaseToken firebaseToken = await FirebaseAuth.GetAuth(_firebaseApp).VerifyIdTokenAsync(idToken);
ClaimsPrincipal claimsPrincipal = new(new List<ClaimsIdentity>
{
new(firebaseToken.Claims
.Select(kvp => new Claim(
kvp.Key,
kvp.Value.ToString() ?? string.Empty)
).ToList(),
nameof(ClaimsIdentity)
)
}
);
return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, JwtBearerDefaults.AuthenticationScheme));
}
catch (Exception exception)
{
return AuthenticateResult.Fail(exception);
}
}
}
FirebaseAuthenticationHandler authenticates requests by validating the Bearer token from the Authorization header, utilizing Firebase SDK to verify ID tokens. Here’s a simplified flow:
FirebaseUserAccessor
FirebaseUserAccessor plays a vital role in extracting user details from claims. It implements IFirebaseUserAccessor, IUserIdAccessor, and ITenantIdAccessor, making it versatile and central in accessing user and tenant details.
领英推荐
public sealed class FirebaseUserAccessor : IFirebaseUserAccessor, IUserIdAccessor, ITenantIdAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IJsonProvider _jsonProvider;
public FirebaseUserAccessor(IHttpContextAccessor httpContextAccessor, IJsonProvider jsonProvider)
{
ArgumentVerifier.NotNull(httpContextAccessor, nameof(httpContextAccessor));
ArgumentVerifier.NotNull(jsonProvider, nameof(jsonProvider));
_httpContextAccessor = httpContextAccessor;
_jsonProvider = jsonProvider;
}
public FirebaseUser? GetFirebaseUser()
{
ClaimsPrincipal? claimsPrincipal = _httpContextAccessor.HttpContext?.User;
if (claimsPrincipal?.Identity is { IsAuthenticated: true })
{
List<Claim> claims = claimsPrincipal.Claims.ToList();
string? id = claims.TryGetClaim("user_id");
string? email = claims.TryGetClaim("email");
bool emailVerified = bool.Parse(claims.TryGetClaim("email_verified") ?? "false");
string? username = claims.TryGetClaim("username");
JsonObject firebase = _jsonProvider.Deserialize<JsonObject>(claims.TryGetClaim("firebase")!);
string? tenant = null;
if (firebase.TryGetPropertyValue("tenant", out JsonNode? jsonNode))
tenant = jsonNode!.GetValue<string>();
return new FirebaseUser(id, email, emailVerified, username, tenant);
}
return null;
}
public string? TryGetUserId() => GetFirebaseUser()?.Id;
public string? TryGetTenantId() => GetFirebaseUser()?.Tenant;
}
FirebaseUserAccessor allows fetching the authenticated user's details, including Id, Email, Username, and Tenant, from the HttpContext. The GetFirebaseUser method retrieves the current authenticated user's information and constructs an FirebaseUser object. The TryGetUserId and TryGetTenantId methods are streamlined ways to access user and tenant IDs.
FirebaseUser
FirebaseUser is a model class representing a Firebase user. It holds various properties of a user, making it essential for managing user data effectively.
public sealed class FirebaseUser
{
public FirebaseUser(
string? id,
string? email,
bool emailVerified,
string? username,
string? tenant
)
{
Id = id;
Email = email;
EmailVerified = emailVerified;
Username = username;
Tenant = tenant;
}
public string? Id { get; }
public string? Email { get; }
public bool EmailVerified { get; }
public string? Username { get; }
public string? Tenant { get; }
}
ServiceCollectionExtensions
This class encompasses the registration of the Firebase App, FirebaseAuthenticationHandler, and FirebaseUserAccessor, orchestrating the integrity of the whole implementation.
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddFirebase(
this IServiceCollection services,
IConfiguration configuration
)
{
ArgumentVerifier.NotNull(services, nameof(services));
ArgumentVerifier.NotNull(configuration, nameof(configuration));
services.Configure<FirebaseOptions>(configuration.GetSection("Firebase"));
services.AddSingleton(sp =>
{
IOptions<FirebaseOptions> optionsAccessor = sp.GetRequiredService<IOptions<FirebaseOptions>>();
return FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromJson(optionsAccessor.Value.Json)
});
});
return services;
}
public static IServiceCollection AddFirebaseAuthenticationHandler(this IServiceCollection services)
{
ArgumentVerifier.NotNull(services, nameof(services));
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddScheme<AuthenticationSchemeOptions, FirebaseAuthenticationHandler>(JwtBearerDefaults.AuthenticationScheme, _ => { });
return services;
}
public static IServiceCollection AddFirebaseUserAccessor(this IServiceCollection services)
{
ArgumentVerifier.NotNull(services, nameof(services));
services
.AddSingleton<IUserIdAccessor, FirebaseUserAccessor>()
.AddSingleton<ITenantIdAccessor, FirebaseUserAccessor>()
.AddSingleton<IFirebaseUserAccessor, FirebaseUserAccessor>();
return services;
}
}
Summary
Firebase multitenancy, in conjunction with .NET 7, allows developers to architect scalable and robust applications that can efficiently serve multiple tenants within the same deployment, ensuring resource optimization and simplified management.
The code below illustrates an implementation where Firebase is crucial for controlling user identities and managing authentication, thereby ensuring that only verified users have access to the secured sections of the application. Multitenancy is seamlessly incorporated at multiple layers in this scenario, facilitating distinctive access and isolation driven by the tenant details connected to Firebase users.
For those interested in delving deeper into the implementation details or wishing to explore the intricacies of the code, the entire codebase featured in this article is available on GitHub.
And, to make things even easier, Carcass.Firebase is available as a NuGet package. You can add it to your project with this command:
dotnet add package Carcass.Firebase
Feel free to explore, use, and contribute to the Carcass project!